Comparing version 0.6.0 to 0.7.0
@@ -6,2 +6,6 @@ All notable changes to this project will be documented in this file. | ||
## [0.7.0] - 2020-03-14 | ||
### Fixed | ||
- support for both native ESM and CommonJS fallback | ||
## [0.6.0] - 2020-03-01 | ||
@@ -8,0 +12,0 @@ ### Added |
@@ -8,8 +8,6 @@ "use strict"; | ||
var _dist = _interopRequireDefault(require("better-custom-error/dist")); | ||
var _betterCustomError = _interopRequireDefault(require("better-custom-error")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const createError = _dist.default.default || _dist.default; // to let it work as bare es modules and common js with babel | ||
/** | ||
@@ -30,4 +28,3 @@ * Base/generic error type | ||
*/ | ||
const HttpError = createError("HttpError"); | ||
const HttpError = (0, _betterCustomError.default)("HttpError"); | ||
/** | ||
@@ -42,3 +39,3 @@ * Response 4xx error | ||
exports.HttpError = HttpError; | ||
const ClientHttpError = createError("ClientHttpError", HttpError); | ||
const ClientHttpError = (0, _betterCustomError.default)("ClientHttpError", HttpError); | ||
/** | ||
@@ -53,3 +50,3 @@ * Response 5xx error | ||
exports.ClientHttpError = ClientHttpError; | ||
const ServerHttpError = createError("ServerHttpError", HttpError); | ||
const ServerHttpError = (0, _betterCustomError.default)("ServerHttpError", HttpError); | ||
/** | ||
@@ -65,3 +62,3 @@ * Response timeout error | ||
exports.ServerHttpError = ServerHttpError; | ||
const TimeoutHttpError = createError("TimeoutHttpError", ServerHttpError); | ||
const TimeoutHttpError = (0, _betterCustomError.default)("TimeoutHttpError", ServerHttpError); | ||
/** | ||
@@ -77,3 +74,3 @@ * Response aborted error | ||
exports.TimeoutHttpError = TimeoutHttpError; | ||
const AbortedHttpError = createError("AbortedHttpError", ClientHttpError); | ||
const AbortedHttpError = (0, _betterCustomError.default)("AbortedHttpError", ClientHttpError); | ||
/** | ||
@@ -87,3 +84,3 @@ * Response data type was different than expected | ||
exports.AbortedHttpError = AbortedHttpError; | ||
const ResponseDataTypeMismatchError = createError("ResponseDataTypeMismatchError"); | ||
const ResponseDataTypeMismatchError = (0, _betterCustomError.default)("ResponseDataTypeMismatchError"); | ||
exports.ResponseDataTypeMismatchError = ResponseDataTypeMismatchError; |
@@ -1,495 +0,6 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.default = void 0; | ||
var _lightIsomorphicFetch = _interopRequireDefault(require("light-isomorphic-fetch")); | ||
var _qs = _interopRequireDefault(require("qs")); | ||
var _urlJoin = _interopRequireDefault(require("url-join")); | ||
var _isomorphicAbortController = _interopRequireDefault(require("isomorphic-abort-controller")); | ||
var _Timeout = _interopRequireDefault(require("oop-timers/src/Timeout")); | ||
var _errors = require("./errors"); | ||
var _response = _interopRequireDefault(require("./response")); | ||
var _matchStatus = require("./response/matchStatus"); | ||
var _request = _interopRequireDefault(require("./request")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
// eslint-disable-line max-lines | ||
const stringify = _qs.default.stringify; | ||
// Types: | ||
// text | ||
// json | ||
// raw (binary) | ||
// stream | ||
/** | ||
* @typedef {Object} ApiOptions | ||
* @property {string} type - expected data type | ||
* @property {Object} headers - headers to be send with each request | ||
* @property {number} retry - how many times should request try to get a successful response. Can be overridden with | ||
* retryPolicy. 1 = no retry, 2 = one retry, etc. | ||
* @property {number} retryInterval - time between retries. Can be overriden with retryWaitPolicy | ||
* @property {function} retryPolicy - function that decides if request should be retried | ||
* @property {function} retryWaitPolicy - function that decides how much time to wait before next retry | ||
* @property {number} timeout - timeout for each request | ||
* @property {number} totalTimeout - total timeout in which all, including retried requests should be fulfilled | ||
* (this includes wait time between, so timeout=100, wait=200, totalTimeout=350 means that retry will have only 50ms) | ||
*/ | ||
const contentTypeMap = { | ||
json: "application/json; charset=utf-8", | ||
text: "application/x-www-form-urlencoded" | ||
}; // const optionsWhitelist = [ | ||
// "method", | ||
// "mode", | ||
// "cache", | ||
// "credentials", | ||
// "headers", | ||
// "redirect", | ||
// "referrer", | ||
// "body", | ||
// ]; | ||
let URLParser = URL; | ||
const safeUrlParse = url => { | ||
try { | ||
return new URLParser(url); | ||
} catch (e) { | ||
// eslint-disable-line no-unused-vars | ||
return null; | ||
} | ||
}; | ||
const globalOptions = { | ||
// eslint-disable-line object-shorthand | ||
retry: 1, | ||
retryInterval: 100, | ||
retryPolicy({ | ||
count | ||
}) { | ||
return count <= this.retry; | ||
}, | ||
retryWaitPolicy() { | ||
return this.retryInterval; | ||
}, | ||
timeout: 30000, | ||
totalTimeout: 60000 | ||
}; | ||
const wait = time => new Promise(resolve => setTimeout(resolve, time)); | ||
const noop = () => undefined; | ||
const createAbortError = ({ | ||
isTimeouted, | ||
isGlobalTimeouted, | ||
lastError, | ||
errorDetails | ||
}) => { | ||
const useTimeoutError = isTimeouted || isGlobalTimeouted; | ||
if (useTimeoutError) { | ||
return new _errors.TimeoutHttpError(`Request aborted because of timeout`, lastError, errorDetails); | ||
} | ||
return new _errors.AbortedHttpError(`Request aborted`, lastError, errorDetails); | ||
}; | ||
class ApiClient { | ||
/** | ||
* @class ApiClient | ||
* @param {ApiOptions} options - options that will override defaults | ||
*/ | ||
constructor(options) { | ||
// @todo validate them? | ||
this._options = options || {}; | ||
} | ||
_getType(options) { | ||
return options.type || this._options.type || "json"; // @todo do not hardcode type here | ||
} | ||
_getContentType(options) { | ||
const type = this._getType(options); | ||
return contentTypeMap[type]; // @todo handle unknown type | ||
} | ||
_getBody(options, body) { | ||
const type = this._getType(options); | ||
if (type === "json") { | ||
return JSON.stringify(body); | ||
} | ||
if (type === "text") { | ||
if (typeof body === "string") { | ||
return body; | ||
} | ||
return stringify(body); | ||
} | ||
return ""; // @todo throw? | ||
} | ||
_buildFetchOptions(options, body) { | ||
const globalHeaders = this._options.headers; | ||
const localHeaders = options.headers; | ||
const contentType = {}; | ||
const bodyOptions = {}; | ||
if (body != null) { | ||
contentType["Content-Type"] = this._getContentType(options); | ||
bodyOptions.body = this._getBody(options, body); | ||
} | ||
return { // @todo filter only known options | ||
...globalOptions, | ||
...this._options, | ||
...options, | ||
...bodyOptions, | ||
headers: { ...globalHeaders, | ||
// @todo handle same header but with different case | ||
...localHeaders, | ||
// @todo handle multiple headers | ||
...contentType | ||
} | ||
}; | ||
} | ||
_buildUrlBase(url, base) { | ||
const parsedBase = safeUrlParse(base); | ||
if (!parsedBase || !parsedBase.host) { | ||
// @todo throw an Error ? | ||
return url; | ||
} | ||
const parsedUrl = safeUrlParse(url); | ||
if (parsedUrl && parsedUrl.base) { | ||
// base is valid full url and given url is also full url | ||
throw new Error("Cannot use absolute url with base url."); // @todo throw custom type? | ||
} | ||
return (0, _urlJoin.default)(base, url); | ||
} | ||
_buildUrl(originalUrl, queryParams, fetchOptions) { | ||
const url = this._buildUrlBase(originalUrl, fetchOptions.base); | ||
if (!queryParams) { | ||
return url; | ||
} | ||
const hasQS = url.includes("?"); | ||
const appendChar = hasQS ? "&" : "?"; // @todo extract existing query params from string and include for stringify ? | ||
// @todo add support for string query params | ||
return url + appendChar + stringify(queryParams); | ||
} | ||
/** | ||
* Sends a GET request | ||
* | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
get(url, queryParams, options) { | ||
return this.request("GET", url, queryParams, null, options); | ||
} | ||
/** | ||
* Sends a POST request | ||
* | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {string|Object} [body] - request body. Used as-is when string or stringified according to given data | ||
* `type` when Object | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
post(url, queryParams, body, options) { | ||
return this.request("POST", url, queryParams, body, options); | ||
} | ||
/** | ||
* Sends a PATCH request | ||
* | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {string|Object} [body] - request body. Used as-is when string or stringified according to given data | ||
* `type` when Object | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
patch(url, queryParams, body, options) { | ||
return this.request("PATCH", url, queryParams, body, options); | ||
} | ||
/** | ||
* Sends a DELETE request | ||
* | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {string|Object} [body] - request body. Used as-is when string or stringified according to given data | ||
* `type` when Object | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
delete(url, queryParams, body, options) { | ||
return this.request("DELETE", url, queryParams, body, options); | ||
} | ||
/** | ||
* Sends a HEAD request | ||
* | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {string|Object} [body] - request body. Used as-is when string or stringified according to given data | ||
* `type` when Object | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
head(url, queryParams, body, options) { | ||
return this.request("HEAD", url, queryParams, body, options); | ||
} | ||
/** | ||
* Sends a custom method request | ||
* | ||
* @param {string} method - method to use | ||
* @param {string} url - absolute url or relative that will be joined with base url | ||
* @param {Object|null} [queryParams] - query params that will be added to `url` | ||
* @param {string|Object} [body] - request body. Used as-is when string or stringified according to given data | ||
* `type` when Object | ||
* @param {ApiOptions} [options] - options that will override defaults and options specified in the constructor | ||
* @returns {Promise<Response>} | ||
* @throws {ClientHttpError} | ||
* @throws {ServerHttpError} | ||
* @throws {ResponseDataTypeMismatchError} | ||
* @throws {AbortedHttpError} | ||
* @throws {TimeoutHttpError} | ||
* @throws {Error} | ||
*/ | ||
request(method, url, queryParams, body, options = {}) { | ||
// eslint-disable-line max-lines-per-function | ||
const start = Date.now(); | ||
const fineOptions = this._buildFetchOptions(options || {}, body); | ||
let currentController, | ||
globalTimeout, | ||
aborted = false, | ||
isGlobalTimeouted = false; | ||
const globalBreak = () => { | ||
isGlobalTimeouted = true; | ||
future.abort(); // eslint-disable-line no-use-before-define | ||
}; | ||
const future = new Promise((resolve, reject) => { | ||
// eslint-disable-line max-lines-per-function | ||
let count = 0, | ||
lastError = null; | ||
return (async () => { | ||
// eslint-disable-line max-statements, max-lines-per-function | ||
while (fineOptions.retryPolicy({ | ||
count: ++count | ||
})) { | ||
let isTimeouted = false; | ||
currentController = new _isomorphicAbortController.default(); | ||
const singleTimeout = new _Timeout.default(() => { | ||
// eslint-disable-line no-loop-func | ||
isTimeouted = true; | ||
currentController.abort(); | ||
}, fineOptions.timeout); | ||
try { | ||
if (count > 1) { | ||
const waitTime = fineOptions.retryWaitPolicy({ | ||
count | ||
}); | ||
if (!globalTimeout || fineOptions.totalTimeout > Date.now() - start + waitTime) { | ||
await wait(waitTime); | ||
} else { | ||
globalTimeout.stop(); | ||
globalBreak(); | ||
} | ||
} | ||
if (aborted) { | ||
const errorDetails = { | ||
tries: count - 1, | ||
while: "waiting", | ||
timeout: isTimeouted, | ||
globalTimeout: isGlobalTimeouted | ||
}; | ||
lastError = createAbortError({ | ||
isTimeouted, | ||
isGlobalTimeouted, | ||
lastError, | ||
errorDetails | ||
}); | ||
break; | ||
} | ||
singleTimeout.start(); | ||
return await this._request(method, url, queryParams, fineOptions, currentController.signal); | ||
} catch (e) { | ||
if (e.name === "AbortError") { | ||
const errorDetails = { | ||
tries: count, | ||
while: "connection", | ||
timeout: isTimeouted, | ||
globalTimeout: isGlobalTimeouted | ||
}; | ||
lastError = createAbortError({ | ||
isTimeouted, | ||
isGlobalTimeouted, | ||
lastError, | ||
errorDetails | ||
}); // it should not try again if: | ||
// globally timeouted | ||
// aborted by user (abort didn't happened via timeout) | ||
if (isGlobalTimeouted || !isTimeouted) { | ||
break; | ||
} | ||
continue; | ||
} | ||
lastError = e; | ||
} finally { | ||
singleTimeout.stop(); | ||
} | ||
} | ||
throw lastError ? lastError : new Error("No error thrown"); // @todo what to do if no error saved? | ||
})().then(resolve, reject); | ||
}); | ||
future.finally(() => { | ||
globalTimeout && globalTimeout.stop(); | ||
}).catch(noop); // noop is required here, as finally is creating new promise branch and throws if errors occurs | ||
// and each branch should handle error separately (even if that is the same error) | ||
future.abort = () => { | ||
aborted = true; | ||
currentController && currentController.abort(); | ||
}; | ||
if (fineOptions.totalTimeout > 0 && Number.isFinite(fineOptions.totalTimeout)) { | ||
globalTimeout = new _Timeout.default(globalBreak, fineOptions.totalTimeout, true); | ||
} | ||
return future; | ||
} | ||
async _request(method, originalUrl, queryParams, options, signal) { | ||
const fetchOptions = { ...options, | ||
method: method.toUpperCase() | ||
}; | ||
const url = this._buildUrl(originalUrl, queryParams, fetchOptions); | ||
const request = new _request.default(url, fetchOptions, originalUrl, queryParams); | ||
const result = await (0, _lightIsomorphicFetch.default)(request.url, { ...request.options, | ||
signal | ||
}); | ||
const type = this._getType(options || {}); | ||
const response = await (0, _response.default)(result, type, request); | ||
if ("rawBody" in response) { | ||
throw new _errors.ResponseDataTypeMismatchError("Unexpected type of data received", { | ||
response: response, | ||
expectedType: type | ||
}); | ||
} | ||
if ((0, _matchStatus.isClientError)(result.status)) { | ||
throw new _errors.ClientHttpError(result.statusText, { | ||
response | ||
}); | ||
} | ||
if ((0, _matchStatus.isServerError)(result.status)) { | ||
throw new _errors.ServerHttpError(result.statusText, { | ||
response | ||
}); | ||
} | ||
return response; | ||
} | ||
} | ||
/** | ||
* Sets global ApiClient configuration that is shared between instances | ||
* | ||
* @param {Object} options | ||
* @param {function} options.URL - `URL`-compatible URL parser, see: https://is.gd/Wbyu4k and https://is.gd/FziUWo | ||
*/ | ||
ApiClient.configure = options => { | ||
URLParser = options.URL; | ||
}; | ||
var _default = ApiClient; | ||
exports.default = _default; | ||
const m = require("./__index.js"); | ||
const def = m.default; | ||
module.exports = def || {}; | ||
Object.keys(m).filter(key => key !== "default").forEach(key => { | ||
module.exports[key] = m[key]; | ||
}); |
{ | ||
"name": "api-reach", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"repository": "https://github.com/dzek69/api-reach.git", | ||
"author": "Jacek Nowacki @dzek69 <git-public@dzek.eu>", | ||
"license": "MIT", | ||
"main": "src/index.mjs", | ||
"scripts": { | ||
"test": "cross-env NODE_ENV=testing mocha 'src/**/*.spec.js'", | ||
"docs": "node build-scripts/docs && jsdoc -r src README.md -t node_modules/docdash -d ./docs -u ./tutorials -c jsdoc.json && node build-scripts/docs.after", | ||
"transpile": "node build-scripts/transpile && babel src -d dist --ignore **/*.spec.js", | ||
"prepublishOnly": "npm run lint && npm run test && npm run docs", | ||
"prepack": "npm run transpile", | ||
"lint": "cross-env eslint --report-unused-disable-directives 'src/**/*.js' 'src/*.js' 'src/**/*.mjs' 'src/*.mjs'", | ||
"lint:fix": "npm run lint -- --fix" | ||
"test": "cross-env NODE_ENV=testing mocha --require ./test/bootstrap.cjs 'src/**/*.spec.mjs'", | ||
"docs": "node build-scripts/docs.mjs && jsdoc -r src README.md -t node_modules/docdash -d ./docs -u ./tutorials -c jsdoc.json && node build-scripts/docs.after.mjs", | ||
"transpile": "node build-scripts/transpile.mjs && babel src -d dist --ignore **/*.spec.mjs && node build-scripts/transpile.after.mjs", | ||
"prepublishOnly": "yarn lint && yarn test && yarn docs", | ||
"prepack": "yarn transpile", | ||
"lint": "cross-env eslint --report-unused-disable-directives 'src/**/*.mjs' 'src/*.mjs'", | ||
"lint:fix": "yarn lint --fix" | ||
}, | ||
"main": "dist/index.js", | ||
"module": "src/index.mjs", | ||
"exports": { | ||
".": { | ||
"require": "./dist/index.js", | ||
"default": "./src/index.mjs" | ||
} | ||
}, | ||
"devDependencies": { | ||
"@babel/cli": "^7.5.5", | ||
"@babel/core": "^7.5.5", | ||
"@babel/polyfill": "^7.4.4", | ||
"@babel/preset-env": "^7.5.5", | ||
"@babel/register": "^7.5.5", | ||
"@babel/cli": "^7.8.4", | ||
"@babel/core": "^7.8.7", | ||
"@babel/polyfill": "^7.8.7", | ||
"@babel/preset-env": "^7.8.7", | ||
"@babel/register": "^7.8.6", | ||
"@dzek69/eslint-config-base": "^1.0.1", | ||
"babel-plugin-rewire": "^1.2.0", | ||
"cross-env": "^5.2.0", | ||
"docdash": "^1.1.1", | ||
"eslint": "^6.2.2", | ||
"fs-extra": "^7.0.1", | ||
"husky": "^3.0.4", | ||
"cross-env": "^7.0.2", | ||
"docdash": "^1.2.0", | ||
"eslint": "^6.8.0", | ||
"fs-extra": "^8.1.0", | ||
"husky": "^4.2.3", | ||
"jsdoc": "^3.6.3", | ||
"mocha": "^6.2.0", | ||
"mocha": "^6.2.2", | ||
"must": "^0.13.4", | ||
"node-fetch": "^2.3.0" | ||
"node-fetch": "^2.3.0", | ||
"babel-plugin-module-extension": "^0.1.1" | ||
}, | ||
"dependencies": { | ||
"abort-controller": "^2.0.2", | ||
"better-custom-error": "^1.0.0", | ||
"better-custom-error": "^3.0.1", | ||
"isomorphic-abort-controller": "^1.0.0", | ||
"light-isomorphic-fetch": "^1.0.0", | ||
"oop-timers": "^1.0.0", | ||
"oop-timers": "^3.0.1", | ||
"qs": "^6.6.0", | ||
@@ -53,4 +61,5 @@ "url-join": "^4.0.0" | ||
"libraryTemplate": { | ||
"version": "1.9.0" | ||
"version": "2.0.5", | ||
"fixDefaultForCommonJS": true | ||
} | ||
} |
@@ -19,3 +19,2 @@ # api-reach | ||
- add files upload support | ||
- add binary support | ||
- handle multiple same header | ||
@@ -22,0 +21,0 @@ - handle same header, different letters case |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
1387295
67
2174
17
37
+ Addedbetter-custom-error@3.0.1(transitive)
+ Addedoop-timers@3.0.1(transitive)
- Removedbetter-custom-error@1.0.0(transitive)
- Removedoop-timers@1.0.0(transitive)
Updatedbetter-custom-error@^3.0.1
Updatedoop-timers@^3.0.1