frest
Version:
REST client for browser with Fetch
1,015 lines (852 loc) • 26.3 kB
JavaScript
import qs from 'querystringify';
var toString = Object.prototype.toString;
/**
* Remove multiple occurrence of **forward** slash in a string.
* This doesn't remove leading and/or trailing slash.
*
* e.g. "http://example.com/a//b/" -> "http://example.com/a/b/"
*
* @public
* @param input The string to clean.
* @returns The `input` string without multiple occurrence of forward slash.
*/
var trimSlashes = function trimSlashes(input) {
return input.toString().replace(/([^:]\/)\/+/g, '$1');
};
/**
* Utility function to parse a query object into string.
*
* @remarks
* This will only do simple and dumb stringify according to `querystringify`
* package. If you want more control use the package directly instead.
*
* @public
* @param query The query to parse. It can be object/string
* @returns Parsed query string
*/
var parseQuery = function parseQuery(query) {
var q = query || '';
if (typeof q === 'object') {
q = qs.stringify(q, '?');
} else if (q !== '') {
q = q.charAt(0) === '?' ? q : "?" + q;
}
return q;
};
/**
* A TypeScript utility to determine if a potential error is an instance of
* `FrestError`.
*
* @public
* @param e A potential error to check
* @returns true if `e` is an instance of `FrestError`. TypeScript will then provide
* completions of `FrestError` instance type.
*/
var isFrestError = function isFrestError(e) {
return e.frest != null && e.request != null;
};
/**
* Determine if an object is a Buffer.
* @remarks
* Taken from `is-buffer` package by Feross Aboukhadijeh <https://feross.org>,
* licensed under MIT and copied here to provide typings for Frest and
* its users.
*
* {@link https://github.com/feross/is-buffer}
*
* @public
* @param val Value to test.
* @returns true if it's a Buffer.
*/
var isBuffer = function isBuffer(val) {
return val != null && val.constructor != null && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
};
/**
* Determine if a value is an ArrayBuffer
*
* @param val The value to test
* @returns True if value is an ArrayBuffer, otherwise false
*/
var isArrayBuffer = function isArrayBuffer(val) {
return toString.call(val) === '[object ArrayBuffer]';
};
/**
* Determine if a value is a FormData
*
* @param val The value to test
* @returns True if value is an FormData, otherwise false
*/
var isFormData = function isFormData(val) {
return typeof FormData !== 'undefined' && val instanceof FormData;
};
/**
* Determine if a value is a view on an ArrayBuffer
*
* @param val The value to test
* @returns True if value is a view on an ArrayBuffer, otherwise false
*/
var isArrayBufferView = function isArrayBufferView(val) {
var result;
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {
result = ArrayBuffer.isView(val);
} else {
result = val && val.buffer && val.buffer instanceof ArrayBuffer;
}
return result;
}; // These utilities below are all taken from axios' source authored by
// Matt Zabriskie under MIT License.
/**
* Determine if a value is an Object
*
* @param val The value to test
* @returns True if value is an Object, otherwise false
*/
var isObject = function isObject(val) {
return val !== null && typeof val === 'object';
};
/**
* Determine if a value is a File
*
* @param val The value to test
* @returns True if value is a File, otherwise false
*/
var isFile = function isFile(val) {
return toString.call(val) === '[object File]';
};
/**
* Determine if a value is a Blob
*
* @param val The value to test
* @returns True if value is a Blob, otherwise false
*/
var isBlob = function isBlob(val) {
return toString.call(val) === '[object Blob]';
};
/**
* Determine if a value is a Function
*
* @param val The value to test
* @returns True if value is a Function, otherwise false
*/
var isFunction = function isFunction(val) {
return toString.call(val) === '[object Function]';
};
/**
* Determine if a value is a Stream
*
* @param val The value to test
* @returns True if value is a Stream, otherwise false
*/
var isStream = function isStream(val) {
return isObject(val) && isFunction(val.pipe);
};
/**
* Determine if a value is a URLSearchParams object
*
* @param val The value to test
* @returns True if value is a URLSearchParams object, otherwise false
*/
var isURLSearchParams = function isURLSearchParams(val) {
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams;
};
var utils = /*#__PURE__*/Object.freeze({
__proto__: null,
trimSlashes: trimSlashes,
parseQuery: parseQuery,
isFrestError: isFrestError,
isBuffer: isBuffer,
isArrayBuffer: isArrayBuffer,
isFormData: isFormData,
isArrayBufferView: isArrayBufferView,
isObject: isObject,
isFile: isFile,
isBlob: isBlob,
isFunction: isFunction,
isStream: isStream,
isURLSearchParams: isURLSearchParams
});
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _construct(Parent, args, Class) {
if (_isNativeReflectConstruct()) {
_construct = Reflect.construct;
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
/**
* @module frest
*/
/**
* FrestError constructor/class signature.
* @remarks
* This is only used for UMD build.
* @public
*/
/**
* Error representation class when there is any failure during request life-cycle.
* @public
*/
var FrestError = /*#__PURE__*/function (_Error) {
_inheritsLoose(FrestError, _Error);
/* istanbul ignore next */
function FrestError(message, frest, request, response) {
var _this;
_this = _Error.call(this, message) || this;
_this.frest = frest;
_this.request = request;
_this.response = response;
return _this;
}
return FrestError;
}( /*#__PURE__*/_wrapNativeSuper(Error));
/**
* @module frest
* @hidden
*/
// These were taken from somewhere, made by someone.
// I adopted it to frest but forgot to put any reference to original source.
// If someone recognized these code and know where the original code came from,
// please raise an issue so that I can put attribution here.
var supportBlob = function supportBlob() {
return FileReader !== undefined && Blob !== undefined && function () {
try {
// tslint:disable-next-line:no-unused-expression
new Blob();
return true;
} catch (error) {
return false;
}
}();
};
function parseHeaders(rawHeaders) {
var headers = new Headers(); // Replace instances of \r\n and \n followed by at least one space
// or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
preProcessedHeaders.split(/\r?\n/).forEach(function (line) {
var parts = line.split(':');
var key = parts.shift().trim();
if (key) {
var value = parts.join(':').trim();
headers.append(key, value);
}
});
return headers;
}
function xhrFetch(url, conf) {
return new Promise(function (resolve, reject) {
// const request = new Request(url, conf);
var xhr = new XMLHttpRequest();
xhr.onload = function () {
var options = {
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
status: xhr.status,
statusText: xhr.statusText
};
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
var body = !conf.responseType || conf.responseType === 'text' ? xhr.responseText : xhr.response;
resolve(new Response(body, options));
};
xhr.onerror = function () {
reject(new TypeError('Network request failed'));
};
xhr.ontimeout = function () {
reject(new TypeError('Network request failed'));
};
if (xhr.upload && conf.onUploadProgress) {
xhr.upload.onprogress = conf.onUploadProgress;
}
if (conf.onDownloadProgress) {
xhr.onprogress = conf.onDownloadProgress;
}
xhr.open(conf.method, url, true);
if (conf.credentials === 'include') {
xhr.withCredentials = true;
}
xhr.responseType = conf.responseType || 'text';
if (conf.action === 'download') {
if (!conf.responseType && supportBlob()) {
xhr.responseType = 'blob';
}
}
conf.headers.forEach(function (value, name) {
xhr.setRequestHeader(name, value);
});
xhr.send(conf.body);
});
}
var InterceptorManager = /*#__PURE__*/function () {
function InterceptorManager() {
this._int = [];
}
var _proto = InterceptorManager.prototype;
_proto.use = function use(handler) {
this._int.push(handler);
return this._int.length - 1;
};
_proto.eject = function eject(id) {
this._int.splice(id, 1);
};
_createClass(InterceptorManager, [{
key: "handlers",
get: function get() {
return this._int;
}
}]);
return InterceptorManager;
}();
var _excluded = ["fetch", "base", "method", "headers"];
var methodsNoData = ['get', 'delete', 'options', 'download'];
var methodsData = ['post', 'put', 'patch', 'upload'];
var setCt = function setCt(headers, value) {
if (!headers.has('Content-Type')) {
headers.set('Content-Type', value);
}
};
var json = function json(raw) {
var ct = raw.headers.get('Content-Type');
if (ct && ct.indexOf('application/json') >= 0) {
return raw.clone().json().catch(function (_) {
return raw.body;
});
}
return raw.body;
};
var req = function req(_ref, data) {
var headers = _ref.headers;
if (isFormData(data) || isArrayBuffer(data) || isBuffer(data) || isStream(data) || isFile(data) || isBlob(data)) {
return data;
}
if (isArrayBufferView(data)) {
return data.buffer;
}
if (isURLSearchParams(data)) {
setCt(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (isObject(data)) {
setCt(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
};
var resp = function resp(raw, // tslint:disable-next-line:no-object-literal-type-assertion
data) {
if (data === void 0) {
data = {};
}
return {
raw: raw,
data: data,
headers: raw.headers,
ok: raw.ok,
redirected: raw.redirected,
status: raw.status,
statusText: raw.statusText,
trailer: raw.trailer,
type: raw.type,
url: raw.url
};
};
var checkInt = function checkInt(ret, type) {
if (!ret) {
var w = type === 'request' ? type + " config" : type;
throw new Error("one of interceptor didn't return " + w);
}
};
/**
* Default configuration if Frest instance is created without any configuration.
* @public
*/
var DEFAULT_CONFIG = {
base: '',
fetch: fetch,
headers: {
common: new Headers({
Accept: 'application/json, text/plain, */*'
}),
post: new Headers(),
get: new Headers(),
put: new Headers(),
delete: new Headers(),
patch: new Headers(),
options: new Headers()
},
method: 'GET',
transformResponse: [json],
transformRequest: [req]
};
/**
* Frest constructor/class signature.
* @remarks
* This is only used for UMD build.
* @public
*/
/**
* The main Frest class.
* @public
*/
var Frest = /*#__PURE__*/function () {
/**
* Creates an instance of Frest.
* @param config - Configuration for this instance.
* Can be string or array of string (in which it'll be the `base` URL for
* every requests), or a {@link Config} object. Defaults to `DEFAULT_CONFIG`
*/
function Frest(config) {
var _this = this;
this.config = void 0;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
error: new InterceptorManager()
};
this.req = function (request) {
var fetchFn;
try {
fetchFn = _this.getFetch(request);
} catch (error) {
return Promise.reject(error);
}
var fullPath = _this.parsePath(request.path, request.query);
return fetchFn(fullPath, request).then(function (raw) {
return {
request: request,
raw: raw
};
});
};
this.after = function (afterFetch) {
var raw = afterFetch.raw,
request = afterFetch.request;
var dataPromise = Promise.resolve({});
var _loop = function _loop(i) {
dataPromise = dataPromise.then(function (data) {
return request.transformResponse[i](raw, data);
});
};
for (var i = 0; i < request.transformResponse.length; i++) {
_loop(i);
}
if (!raw.ok) {
return dataPromise.then(function (data) {
return Promise.reject(new FrestError("Non OK HTTP response status: " + raw.status + " - " + raw.statusText, _this, request, resp(raw, data)));
});
}
var responsePromise = dataPromise.then(function (data) {
return resp(raw, data);
});
var _loop2 = function _loop2(_i) {
responsePromise = responsePromise.then(function (response) {
checkInt(response, 'response');
return _this.interceptors.response.handlers[_i]({
frest: _this,
request: request,
response: response
});
});
};
for (var _i = 0; _i < _this.interceptors.response.handlers.length; _i++) {
_loop2(_i);
}
return responsePromise.then(function (r) {
checkInt(r, 'response');
return r;
}).catch(function (e) {
var cause = typeof e === 'string' ? e : e.message ? e.message : e;
return Promise.reject(new FrestError("Error in response transform/interceptor: " + cause, _this, request, resp(raw)));
});
};
this.onError = function (request) {
return function (e) {
var err = _this.toFrestError(e, request);
if (_this.interceptors.error.handlers.length === 0) {
return Promise.reject(err);
}
return new Promise(function (resolve, reject) {
var promise = Promise.resolve(null);
var recovery = null;
var _loop3 = function _loop3(i) {
if (recovery != null) {
return "break";
}
promise = promise // eslint-disable-next-line no-loop-func
.then(function (rec) {
if (rec != null) {
recovery = rec;
return rec;
}
return _this.interceptors.error.handlers[i](err);
}) // eslint-disable-next-line no-loop-func
.catch(function (ee) {
err = _this.toFrestError(ee, request);
return null;
});
};
for (var i = 0; i < _this.interceptors.error.handlers.length; i++) {
var _ret = _loop3(i);
if (_ret === "break") break;
}
promise.then(function (res) {
if (res) {
resolve(res);
} else {
reject(err);
}
});
});
};
};
if (config && typeof config === 'string') {
this.config = _extends({}, DEFAULT_CONFIG, {
base: config
});
} else if (config && typeof config === 'object') {
var headers = _extends({}, DEFAULT_CONFIG.headers, config.headers);
this.config = _extends({}, DEFAULT_CONFIG, config, {
headers: headers
});
} else {
this.config = _extends({}, DEFAULT_CONFIG);
}
this.config.base = trimSlashes(this.config.base);
}
/**
* Get base URL used in this instance.
*/
var _proto = Frest.prototype;
_proto.create = function create(config) {
return new Frest(config);
}
/**
* Merge this instance config with the one provided here.
* @param config - Configuration to be merged into this instance's configuration.
*/
;
_proto.mergeConfig = function mergeConfig(config) {
var headers = _extends({}, this.config.headers, config.headers);
this.config = _extends({}, this.config, config, {
headers: headers
});
}
/**
* Get `fetch` function used in this instance.
* @remarks
* This can be the native `fetch` API or any function with similar signature.
*/
;
/**
* Get full URL from the provided path and query object/string.
* @remarks
* This will use the instance's `base` URL configuration and construct full
* URL to the provided arguments.
*
* @param path - Endpoint path
* @param query - query object/string to include
* @returns Full URL to the provided arguments.
*/
_proto.parsePath = function parsePath(path, query) {
var paths = path ? path instanceof Array ? path : [path] : [''];
query = this.parseQuery(query);
return trimSlashes(this.config.base + "/" + paths.map(encodeURI).join('/') + query);
}
/**
* Utility function to parse a query object into string.
*
* @remarks
* This is a shortcut to the `utils.parseQuery` function.
*
* @param query - The query to parse. It can be object/string
* @returns Parsed query string
*/
;
_proto.parseQuery = function parseQuery$1(query) {
return parseQuery(query);
}
/**
* Make a request to an endpoint.
*
* @template T - The type of response's body, if any. Defaults to `any`.
* @param init - A string, string array, or request configuration object.
* @param request - request configuration if the first arg is string
* or string array
* @returns Response promise which will be resolved when the request is successful.
* The promise will throws in case of error in any request life-cycle.
*/
;
_proto.request = function (_request) {
function request(_x) {
return _request.apply(this, arguments);
}
request.toString = function () {
return _request.toString();
};
return request;
}(function (init, request) {
if (request === void 0) {
request = {};
}
var conf = _extends({
action: 'request',
method: this.config.method
}, this.requestConfig(init, request));
return this.internalRequest(_extends({}, conf, {
headers: this.headers(conf)
}));
});
_proto.internalRequest = function internalRequest(request) {
return this.before(request).then(this.req).then(this.after).catch(this.onError(request));
};
_proto.requestConfig = function requestConfig(init, request) {
var _this$config = this.config,
fetch = _this$config.fetch,
base = _this$config.base,
method = _this$config.method,
headers = _this$config.headers,
rest = _objectWithoutPropertiesLoose(_this$config, _excluded);
if (typeof init === 'string' || init instanceof Array) {
return _extends({
path: init
}, rest, request);
}
return _extends({
path: ''
}, rest, init);
};
_proto.headers = function headers(request) {
var method = request.method.toLowerCase();
var headers = new Headers(this.config.headers.common);
this.config.headers[method].forEach(function (v, k) {
headers.set(k, v);
});
if (request.headers) {
request.headers.forEach(function (v, k) {
headers.set(k, v);
});
}
return headers;
};
_proto.getFetch = function getFetch(request) {
if (request.action === 'upload' || request.action === 'download') {
return xhrFetch;
}
if (typeof request.fetch === 'function') {
return request.fetch;
} else if (typeof this.config.fetch === 'function') {
return this.config.fetch;
}
throw new FrestError('Fetch API is not available in this browser', this, request);
};
_proto.before = function before(request) {
var _this2 = this;
return new Promise(function (resolve, reject) {
var dataPromise = Promise.resolve(request.body);
var _loop4 = function _loop4(i) {
if (methodsData.indexOf(request.method.toLowerCase()) >= 0) {
dataPromise = dataPromise.then(function (data) {
return request.transformRequest[i](request, data);
});
}
};
for (var i = 0; i < request.transformRequest.length; i++) {
_loop4(i);
}
var requestPromise = dataPromise.then(function (body) {
request.body = body;
return request;
});
var _loop5 = function _loop5(_i2) {
requestPromise = requestPromise.then(function (requestConfig) {
checkInt(requestConfig, 'request');
return _this2.interceptors.request.handlers[_i2]({
frest: _this2,
request: requestConfig
});
});
};
for (var _i2 = 0; _i2 < _this2.interceptors.request.handlers.length; _i2++) {
_loop5(_i2);
}
requestPromise.then(function (requestConfig) {
checkInt(requestConfig, 'request');
resolve(requestConfig);
}).catch(function (e) {
var cause = typeof e === 'string' ? e : e.message ? e.message : e;
reject(new FrestError("Error in request transform/interceptor: " + cause, _this2, request));
});
});
};
_proto.toFrestError = function toFrestError(e, requestConfig) {
return isFrestError(e) ? new FrestError(e.message, this, requestConfig, e.response) : e;
};
_createClass(Frest, [{
key: "base",
get: function get() {
return this.config.base;
}
}, {
key: "fetchFn",
get: function get() {
return this.config.fetch;
}
}]);
return Frest;
}();
var _loop6 = function _loop6(i) {
var method = methodsNoData[i];
var meth = method === 'download' ? 'GET' : method.toUpperCase();
Frest.prototype[method] = function (init, request) {
if (request === void 0) {
request = {};
}
var conf = _extends({
action: method,
method: meth
}, this.requestConfig(init, request));
return this.internalRequest(_extends({}, conf, {
headers: this.headers(conf)
}));
};
};
for (var i = 0; i < methodsNoData.length; i++) {
_loop6(i);
}
var _loop7 = function _loop7(_i3) {
var method = methodsData[_i3];
var meth = method === 'upload' ? 'POST' : method.toUpperCase();
Frest.prototype[method] = function (init, body, request) {
if (request === void 0) {
request = {};
}
var conf = _extends({
action: method,
method: meth
}, this.requestConfig(init, request));
return this.internalRequest(_extends({
body: body
}, conf, {
headers: this.headers(conf)
}));
};
};
for (var _i3 = 0; _i3 < methodsData.length; _i3++) {
_loop7(_i3);
}
/**
* @module frest
* @preferred
*
* Main frest module.
*/
var xhrFetch$1 = xhrFetch;
var frest = new Frest();
export default frest;
export { DEFAULT_CONFIG, Frest, FrestError, utils, xhrFetch$1 as xhrFetch };