UNPKG

frest

Version:

REST client for browser with Fetch

1,025 lines (859 loc) 26.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var qs = _interopDefault(require('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(); exports.DEFAULT_CONFIG = DEFAULT_CONFIG; exports.Frest = Frest; exports.FrestError = FrestError; exports.default = frest; exports.utils = utils; exports.xhrFetch = xhrFetch$1;