UNPKG

hapic

Version:

A http api client based on axios.

1,027 lines (995 loc) 32.1 kB
import { withBase, withQuery } from 'ufo'; import nodeFetch, { File as File$1, FormData as FormData$1, AbortController as AbortController$1, Headers as Headers$1, Blob as Blob$1 } from 'node-fetch-native'; import { BaseError } from 'ebec'; var o = Object.defineProperty; var e = (t, r)=>o(t, "name", { value: r, configurable: true }); var f = Object.defineProperty, c = e((t, r)=>f(t, "name", { value: r, configurable: true }), "e"); function n() { return { agent: void 0, dispatcher: void 0 }; } e(n, "createProxy"), c(n, "createProxy"); function a() { return globalThis.fetch; } e(a, "createFetch"), c(a, "createFetch"); const gT = (()=>{ if (typeof globalThis !== 'undefined') { return globalThis; } // eslint-disable-next-line no-restricted-globals if (typeof self !== 'undefined') { // eslint-disable-next-line no-restricted-globals return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); })(); const File = gT.File || File$1; const FormData = gT.FormData || FormData$1; const AbortController = gT.AbortController || AbortController$1; const fetch = gT.fetch || nodeFetch; const Headers = gT.Headers || Headers$1; const Blob = gT.Blob || Blob$1; function createProxy(options) { return n(); } /* * Copyright (c) 2023. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var ResponseType = /*#__PURE__*/ function(ResponseType) { ResponseType["BLOB"] = "blob"; ResponseType["STREAM"] = "stream"; ResponseType["TEXT"] = "text"; ResponseType["ARRAY_BUFFER"] = "arrayBuffer"; ResponseType["JSON"] = "json"; return ResponseType; }({}); var MethodName = /*#__PURE__*/ function(MethodName) { MethodName["DELETE"] = "DELETE"; MethodName["GET"] = "GET"; MethodName["HEAD"] = "HEAD"; MethodName["PATCH"] = "PATCH"; MethodName["POST"] = "POST"; MethodName["PUT"] = "PUT"; return MethodName; }({}); /* * Copyright (c) 2023. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var HookName = /*#__PURE__*/ function(HookName) { HookName["REQUEST"] = "request"; HookName["REQUEST_ERROR"] = "requestError"; HookName["RESPONSE"] = "response"; HookName["RESPONSE_ERROR"] = "responseError"; return HookName; }({}); /* * Copyright (c) 2021. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ // eslint-disable-next-line @typescript-eslint/ban-types function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function kindOf(input) { return Object.prototype.toString.call(input).slice(8, -1).toLowerCase(); } function kindOfTest(type, input) { type = type.toLowerCase(); return kindOf(input) === type; } function isObject(input) { return typeof input === 'object' && input !== null && !!input && !Array.isArray(input); } function isStream(input) { return isObject(input) && typeof input.pipe === 'function'; } function isFormData(input) { if (typeof FormData !== 'undefined' && input instanceof FormData) { return true; } const pattern = '[object FormData]'; if (Object.prototype.toString.call(input) === pattern) { return true; } return !!input && typeof input.toString === 'function' && input.toString() === pattern; } function isArrayBuffer(input) { if (typeof ArrayBuffer !== 'undefined' && input instanceof ArrayBuffer) { return true; } return kindOfTest('ArrayBuffer', input); } function isFile(input) { if (typeof File !== 'undefined' && input instanceof File) { return true; } return kindOfTest('File', input); } function isBlob(input) { if (typeof Blob !== 'undefined' && input instanceof Blob) { return true; } return kindOfTest('Blob', input); } function isURLSearchParams(input) { if (typeof URLSearchParams !== 'undefined' && input instanceof URLSearchParams) { return true; } return kindOfTest('URLSearchParams', input); } function verifyInstanceBySymbol(input, name) { if (!isObject(input) && typeof input !== 'function') { return false; } return input['@instanceof'] === Symbol.for(name); } function isSerializable(input) { if (input === undefined) { return false; } const t = typeof input; if (t === 'string' || t === 'number' || t === 'boolean' || t === null || t === 'bigint') { return true; } if (t !== 'object') { return false; // function, symbol, undefined } if (Array.isArray(input)) { return true; } return isObject(input) || typeof input === 'function' && input.toJSON === 'function'; } function serialize(input) { return JSON.stringify(input, (_key, value)=>typeof value === 'bigint' ? value.toString() : value); } /* * Copyright (c) 2023. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ function isPromise(p) { return typeof p === 'object' && p !== null && (typeof Promise !== 'undefined' && p instanceof Promise || // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore typeof p.then === 'function'); } /** * Traverse object or array and provide the ability to replace values. * * @param input * @param fn */ function traverse(input, fn) { if (Array.isArray(input)) { for(let i = 0; i < input.length; i++){ if (isObject(input[i]) || Array.isArray(input[i])) { input[i] = fn(traverse(input[i], fn), `${i}`); continue; } input[i] = fn(input[i], `${i}`); } return input; } const keys = Object.keys(input); for(let i = 0; i < keys.length; i++){ const value = input[keys[i]]; if (isObject(value) || Array.isArray(value)) { input[keys[i]] = fn(traverse(value, fn), keys[i]); continue; } input[keys[i]] = fn(value, keys[i]); } return input; } /* * Copyright (c) 2022. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var AuthorizationHeaderType = /*#__PURE__*/ function(AuthorizationHeaderType) { AuthorizationHeaderType["BEARER"] = "Bearer"; AuthorizationHeaderType["BASIC"] = "Basic"; AuthorizationHeaderType["X_API_KEY"] = "X-API-Key"; AuthorizationHeaderType["API_KEY"] = "API-Key"; return AuthorizationHeaderType; }({}); /* * Copyright (c) 2022. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var ErrorCode = /*#__PURE__*/ function(ErrorCode) { ErrorCode["AUTHORIZATION_HEADER_INVALID"] = "auth_header_invalid"; ErrorCode["AUTHORIZATION_HEADER_PARSE"] = "auth_header_parse"; ErrorCode["AUTHORIZATION_HEADER_TYPE_PARSE"] = "auth_header_type_parse"; ErrorCode["CONNECTION_ABORTED"] = "ECONNABORTED"; ErrorCode["CONNECTION_CLOSED"] = "ECONNRESET"; return ErrorCode; }({}); class AuthorizationHeaderError extends BaseError { /* istanbul ignore next */ static parse() { throw new AuthorizationHeaderError({ code: ErrorCode.AUTHORIZATION_HEADER_PARSE, message: 'The authorization header value could not be parsed.' }); } /* istanbul ignore next */ static parseType() { throw new AuthorizationHeaderError({ code: ErrorCode.AUTHORIZATION_HEADER_TYPE_PARSE, message: 'The authorization header value type must either be \'Bearer\' or \'Basic\'' }); } constructor(options){ super({ code: ErrorCode.AUTHORIZATION_HEADER_INVALID, message: 'The authorization header is not valid.' }, options || {}); } } class ClientError extends BaseError { constructor(ctx){ super({ cause: ctx.error }), this['@instanceof'] = Symbol.for('ClientError'); this.request = ctx.request; this.response = ctx.response; this.code = ctx.code; this.status = ctx.response && ctx.response.status; this.statusCode = ctx.response && ctx.response.status; this.statusMessage = ctx.response && ctx.response.statusText; this.statusText = ctx.response && ctx.response.statusText; this.message = ctx.message; if (Error.captureStackTrace) { Error.captureStackTrace(this, ClientError); } } } function isClientError(error) { if (error instanceof ClientError) { return true; } return isObject(error) && verifyInstanceBySymbol(error, 'ClientError'); } function isClientErrorWithStatusCode(error, statusCode) { if (!isClientError(error) || !isObject(error.response)) { return false; } const statusCodes = Array.isArray(statusCode) ? statusCode : [ statusCode ]; for(let i = 0; i < statusCodes.length; i++){ if (statusCodes[i] === error.response.status) { return true; } } return false; } function isClientErrorDueNetworkIssue(error) { return isClientError(error) && !error.response && Boolean(error.code) && error.code !== ErrorCode.CONNECTION_ABORTED; } function formatRequestOptions(input) { if (input.url) { const parts = []; if (input.method) { parts.push(input.method); } parts.push(input.url); return parts.join(' '); } return input.toString(); } function createClientError(context) { let message; if (context.request && context.response) { message = `${context.response.status} ${context.response.statusText} (${formatRequestOptions(context.request)})`; } else if (context.request) { message = `${formatRequestOptions(context.request)}`; } if (context.error) { if (context.error.message) { message = `${context.error.message} (${message})`; } } if (!message) { message = 'An unknown error occurred.'; } const isAbort = !!context.error && context.error.name === 'AbortError'; let code; if (!context.response) { if (isAbort) { code = ErrorCode.CONNECTION_ABORTED; } else { code = ErrorCode.CONNECTION_CLOSED; } } const error = new ClientError({ ...context, code, message }); if (Error.captureStackTrace) { Error.captureStackTrace(error, createClientError); } return error; } function toError(input) { if (input instanceof Error) { return input; } if (isObject(input)) { if (input.constructor.name === 'TypeError') { Object.setPrototypeOf(input, TypeError.prototype); return input; } const error = new Error(input.message); extendError(error, input); return input; } return undefined; } function extendError(error, data) { const keys = Object.getOwnPropertyNames(data); for(let i = 0; i < keys.length; i++){ error[keys[i]] = data[keys[i]]; } } function parseAuthorizationHeader(value) { /* istanbul ignore next */ if (typeof value !== 'string') { throw AuthorizationHeaderError.parse(); } const parts = value.split(' '); if (parts.length < 2) { throw AuthorizationHeaderError.parseType(); } const type = parts[0].toLowerCase(); const id = parts[1]; switch(type){ case 'basic': { const base64Decoded = Buffer.from(id, 'base64').toString('utf-8'); const base64Parts = base64Decoded.split(':'); if (base64Parts.length !== 2) { throw AuthorizationHeaderError.parse(); } return { type: AuthorizationHeaderType.BASIC, username: base64Parts[0], password: base64Parts[1] }; } case 'bearer': return { type: AuthorizationHeaderType.BEARER, token: id }; case 'api-key': case 'x-api-key': return { type: type === 'api-key' ? AuthorizationHeaderType.API_KEY : AuthorizationHeaderType.X_API_KEY, key: id }; default: throw AuthorizationHeaderError.parseType(); } } function stringifyAuthorizationHeader(header) { switch(header.type){ case AuthorizationHeaderType.BASIC: { const basicStr = Buffer.from(`${header.username}:${header.password}`).toString('base64'); return `Basic ${basicStr}`; } case AuthorizationHeaderType.BEARER: return `Bearer ${header.token}`; case AuthorizationHeaderType.X_API_KEY: case AuthorizationHeaderType.API_KEY: return `${header.type} ${header.key}`; } return ''; } /* * Copyright (c) 2023. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var HeaderName = /*#__PURE__*/ function(HeaderName) { HeaderName["ACCEPT"] = "accept"; HeaderName["AUTHORIZATION"] = "authorization"; HeaderName["CONTENT_TYPE"] = "content-type"; return HeaderName; }({}); function setHeader(headers, key, value) { key = key.toLowerCase(); if (typeof Headers !== 'undefined' && headers instanceof Headers) { headers.set(key, value); return; } if (Array.isArray(headers)) { const index = headers.findIndex((el)=>el.length === 2 && el[0].toLowerCase() === key); if (index !== -1) { headers[index] = [ key, value ]; } else { headers.push([ key, value ]); } return; } const keys = Object.keys(headers); const index = keys.findIndex((el)=>el.toLowerCase() === key); if (index !== -1) { headers[keys[index]] = value; } else { headers[key] = value; } } function getHeader(headers, key) { key = key.toLowerCase(); if (typeof Headers !== 'undefined' && headers instanceof Headers) { const value = headers.get(key); return value === null ? undefined : value; } if (Array.isArray(headers)) { const index = headers.findIndex((el)=>el.length === 2 && el[0].toLowerCase() === key); if (index !== -1) { return headers[index][1]; } return undefined; } const keys = Object.keys(headers); const index = keys.findIndex((el)=>el.toLowerCase() === key); if (index !== -1) { return headers[keys[index]]; } return undefined; } function unsetHeader(headers, key) { key = key.toLowerCase(); if (typeof Headers !== 'undefined' && headers instanceof Headers) { headers.delete(key); return; } if (Array.isArray(headers)) { const index = headers.findIndex((el)=>el.length === 2 && el[0].toLowerCase() === key.toLowerCase()); if (index !== -1) { headers.splice(index, 1); } return; } const keys = Object.keys(headers); const index = keys.findIndex((el)=>el.toLowerCase() === key); if (index !== -1) { delete headers[keys[index]]; } } function createDefaultRequestTransformer() { return (data, headers)=>{ if (isFormData(data)) { headers.delete(HeaderName.CONTENT_TYPE); } if (isArrayBuffer(data) || isFile(data) || isBlob(data) || isFormData(data) || isStream(data)) { return data; } if (isURLSearchParams(data)) { headers.set(HeaderName.CONTENT_TYPE, 'application/x-www-form-urlencoded;charset=utf-8'); return data; } const contentType = headers.get(HeaderName.CONTENT_TYPE) || ''; const contentTypeIsJson = contentType.indexOf('application/json') !== -1; if (isSerializable(data) || contentTypeIsJson) { data = typeof data === 'string' ? data : serialize(data); if (!headers.has(HeaderName.CONTENT_TYPE)) { headers.set(HeaderName.CONTENT_TYPE, 'application/json'); } if (!headers.has(HeaderName.ACCEPT)) { headers.set(HeaderName.ACCEPT, 'application/json'); } } return data; }; } function extendRequestOptionsWithDefaults(options) { if (!options.transform) { options.transform = [ createDefaultRequestTransformer() ]; } if (typeof options.proxy === 'undefined') { options.proxy = true; } return options; } function isRequestPayloadSupported(method = 'GET') { method = method.toUpperCase(); return method === MethodName.PATCH || method === MethodName.POST || method === MethodName.PUT; } function isRequestOptions(input) { if (!isObject(input)) { return false; } return input.url === 'string'; } function isResponse(input) { if (!isObject(input)) { return false; } return typeof input.headers !== 'undefined' && typeof input.ok === 'boolean'; } const JSON_REGEX = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i; function detectResponseType(input) { if (typeof input !== 'string' || input.length === 0) { return ResponseType.STREAM; } const contentType = input.split(';').shift() || ''; if (JSON_REGEX.test(contentType)) { return ResponseType.JSON; } const textTypes = [ 'image/svg', 'application/xml', 'application/xhtml', 'application/html' ]; if (textTypes.indexOf(contentType) !== -1 || contentType.startsWith('text/')) { return ResponseType.TEXT; } return ResponseType.JSON; } class HookManager { addListener(name, fn) { this.items[name] = this.items[name] || []; this.items[name].push(fn); return this.items[name].length - 1; } removeListener(name, fn) { if (!this.items[name]) { return; } if (typeof fn === 'number') { this.items[name][fn] = undefined; return; } const index = this.items[name].indexOf(fn); if (index !== -1) { this.items[name][index] = undefined; } } removeListeners(name) { delete this.items[name]; } async triggerReqHook(input) { const items = this.items[HookName.REQUEST] || []; let temp = input; for(let i = 0; i < items.length; i++){ const hook = items[i]; if (!hook) { continue; } let output = hook(temp); if (isPromise(output)) { output = await output; } if (isRequestOptions(output)) { temp = output; } } return temp; } async triggerResHook(input) { const items = this.items[HookName.RESPONSE] || []; let temp = input; for(let i = 0; i < items.length; i++){ const hook = items[i]; if (!hook) { continue; } let output = hook(temp); if (isPromise(output)) { output = await output; } if (isResponse(output)) { temp = output; } } return temp; } async triggerErrorHook(name, input) { const items = this.items[name] || []; let temp = input; for(let i = 0; i < items.length; i++){ const hook = items[i]; if (!hook) { continue; } try { let output = hook(temp); if (isPromise(output)) { output = await output; } if (isResponse(output) || isRequestOptions(output)) { return output; } } catch (e) { temp = e; } } throw temp; } constructor(){ this.items = {}; } } class Client { // --------------------------------------------------------------------------------- /** * Return base url * * @return string */ getBaseURL() { return this.defaults.baseURL; } /** * Overwrite existing base url. * * @param url */ setBaseURL(url) { this.defaults.baseURL = url; return this; } // --------------------------------------------------------------------------------- /** * Set a header for all upcoming requests. * * @param key * @param value */ setHeader(key, value) { this.headers.set(key, value); return this; } /** * Get a header for all upcoming requests. * * @param key */ getHeader(key) { return this.headers.get(key); } /** * Unset a specific for all upcoming requests. * * @param key */ unsetHeader(key) { if (this.headers.has(key)) { this.headers.delete(key); } return this; } /** * Unset all defined headers for the upcoming requests. */ unsetHeaders() { this.headers.forEach((_value, key)=>{ this.headers.delete(key); }); return this; } // --------------------------------------------------------------------------------- /** * Set an authorization header (basic, api-key, bearer). * * @param options */ setAuthorizationHeader(options) { this.setHeader(HeaderName.AUTHORIZATION, stringifyAuthorizationHeader(options)); return this; } /** * Get authorization header. */ getAuthorizationHeader() { const header = this.getHeader(HeaderName.AUTHORIZATION); if (header !== null) { return header; } return undefined; } /** * Unset an authorization header. */ unsetAuthorizationHeader() { this.unsetHeader(HeaderName.AUTHORIZATION); return this; } // --------------------------------------------------------------------------------- /** * Make a custom request. * * @param config */ async request(config) { const headers = new Headers(config.headers); this.headers.forEach((value, key)=>{ if (!headers.has(key)) { headers.set(key, value); } }); const baseURL = config.baseURL || this.defaults.baseURL; let options = { ...this.defaults, ...config, headers, url: baseURL ? withBase(config.url, baseURL) : config.url }; if (options.query || options.params) { options.url = withQuery(options.url, traverse({ ...options.params, ...options.query }, (value)=>{ if (typeof value === 'bigint') { return value.toString(); } return value; })); } options = await this.hookManager.triggerReqHook(options); if (options.transform) { const transformers = Array.isArray(options.transform) ? options.transform : [ options.transform ]; for(let i = 0; i < transformers.length; i++){ const transformer = transformers[i]; options.body = transformer(options.body, options.headers); } } const handleError = async (step, ctx)=>{ const error = createClientError(ctx); if (Error.captureStackTrace) { Error.captureStackTrace(error, this.request); } let output; if (step === 'request') { output = await this.hookManager.triggerErrorHook(HookName.REQUEST_ERROR, error); } else { output = await this.hookManager.triggerErrorHook(HookName.RESPONSE_ERROR, error); } if (output) { if (isResponse(output)) { return output; } return this.request(output); } throw error; }; let response; try { if (!isRequestPayloadSupported(options.method)) { delete options.body; } const { url, proxy, ...data } = options; if (proxy === false) { response = await fetch(url, data); } else { let proxyOptions; if (typeof proxy !== 'boolean' && typeof proxy !== 'undefined') { proxyOptions = proxy; } response = await fetch(url, { ...data, ...createProxy(proxyOptions) }); } } catch (e) { return handleError('request', { request: options, error: toError(e) }); } const responseType = options.responseType || detectResponseType(response.headers.get(HeaderName.CONTENT_TYPE)); let data; switch(responseType){ case ResponseType.STREAM: { data = response.body; break; } case ResponseType.BLOB: { data = await response.blob(); break; } case ResponseType.ARRAY_BUFFER: { data = await response.arrayBuffer(); break; } case ResponseType.TEXT: { data = await response.text(); break; } default: { const temp = await response.text(); try { data = JSON.parse(temp); } catch (e) { data = temp; } } } Object.defineProperty(response, 'data', { get () { return data; }, set (value) { data = value; } }); if (response.status >= 400 && response.status < 600) { return handleError('response', { response, request: options }); } return await this.hookManager.triggerResHook(response); } // --------------------------------------------------------------------------------- /** * Request a resource with the GET method. * * @param url * @param config */ get(url, config) { return this.request({ ...config || {}, method: MethodName.GET, url }); } // --------------------------------------------------------------------------------- /** * Delete a resource with the DELETE method. * * @param url * @param config */ delete(url, config) { return this.request({ ...config || {}, method: MethodName.DELETE, url }); } // --------------------------------------------------------------------------------- /** * Make a verification resource with the HEAD method. * * @param url * @param config */ head(url, config) { return this.request({ ...config || {}, method: MethodName.HEAD, url }); } // --------------------------------------------------------------------------------- /** * Create a resource with the POST method. * * @param url * @param body * @param config */ post(url, body, config) { return this.request({ ...config || {}, method: MethodName.POST, url, body }); } // --------------------------------------------------------------------------------- /** * Update a resource with the PUT method. * * @param url * @param body * @param config */ put(url, body, config) { return this.request({ ...config || {}, method: MethodName.PUT, url, body }); } // --------------------------------------------------------------------------------- /** * Update a resource with the PATCH method. * * @param url * @param body * @param config */ patch(url, body, config) { return this.request({ ...config || {}, method: MethodName.PATCH, url, body }); } on(name, fn) { return this.hookManager.addListener(name, fn); } /** * Remove single or specific hook fn(s). * * @param name * @param fn */ off(name, fn) { if (typeof fn === 'undefined') { this.hookManager.removeListeners(name); return this; } this.hookManager.removeListener(name, fn); return this; } // --------------------------------------------------------------------------------- constructor(input = {}){ this['@instanceof'] = Symbol.for('BaseClient'); this.defaults = extendRequestOptionsWithDefaults(input || {}); this.headers = new Headers(this.defaults.headers); this.hookManager = new HookManager(); } } const instanceMap = {}; function hasClient(key) { return hasOwnProperty(instanceMap, key || 'default'); } function setClient(client, key) { key = key || 'default'; instanceMap[key] = client; return client; } function useClient(key) { key = key || 'default'; if (Object.prototype.hasOwnProperty.call(instanceMap, key)) { return instanceMap[key]; } const instance = createClient(); instanceMap[key] = instance; return instance; } function unsetClient(key) { key = key || 'default'; if (hasOwnProperty(instanceMap, key)) { delete instanceMap[key]; } } function createClient(input) { return new Client(input); } function isClient(input) { if (input instanceof Client) { return true; } return verifyInstanceBySymbol(input, 'Client'); } const client = createClient(); export { AbortController, AuthorizationHeaderError, AuthorizationHeaderType, Blob, Client, ClientError, ErrorCode, File, FormData, HeaderName, Headers, HookManager, HookName, MethodName, ResponseType, createClient, createClientError, createDefaultRequestTransformer, createProxy, client as default, detectResponseType, extendError, extendRequestOptionsWithDefaults, fetch, getHeader, hasClient, hasOwnProperty, isArrayBuffer, isBlob, isClient, isClientError, isClientErrorDueNetworkIssue, isClientErrorWithStatusCode, isFile, isFormData, isObject, isPromise, isRequestOptions, isRequestPayloadSupported, isResponse, isSerializable, isStream, isURLSearchParams, parseAuthorizationHeader, serialize, setClient, setHeader, stringifyAuthorizationHeader, toError, traverse, unsetClient, unsetHeader, useClient, verifyInstanceBySymbol }; //# sourceMappingURL=index.mjs.map