UNPKG

@kth/api-call

Version:

Node.js module to make JSON calls against APIs.

220 lines (203 loc) 6.97 kB
/* eslint-disable no-console */ const fetch = require('node-fetch') const urlJoin = require('url-join') const FormData = require('form-data') const HEADER_USER_AGENT = 'User-Agent' const USER_AGENT = 'KTH api-call/4.*' const MIME_JSON = 'application/json' const MIME_TEXT = 'text/' const MIME_SVG = 'image/svg+xml' const HEADER_CONTENT_LENGTH = 'content-length' const HEADER_CONTENT_TYPE = 'content-type' function removeUndefined(obj) { const returnObj = Object.keys(obj).reduce((acc, key) => { if (obj[key] === undefined) { return acc } acc[key] = obj[key] return acc }, {}) return returnObj } async function _parseResponseBody(response, forceBuffer) { const contentLength = response.headers.get(HEADER_CONTENT_LENGTH) const contentType = response.headers.get(HEADER_CONTENT_TYPE) if (forceBuffer) { return response.buffer() } if (contentLength === '0') { return response.text() } if (contentType?.includes(MIME_JSON)) { const buffer = await response.buffer() try { return JSON.parse(buffer.toString()) } catch (error) { return buffer.toString() } } if (contentType?.includes(MIME_SVG)) { return response.text() } if (contentType?.includes(MIME_TEXT)) { return response.text() } return response.buffer() } function buildFormData(data) { const form = new FormData() for (const key in data) { if (key === 'file') { form.append(key, data[key].value, data[key].options) } else { form.append(key, data[key]) } } return form } function _createFetchWrapper(wrapperOptions, method) { const { baseUrl = '', headers = {}, json = true } = wrapperOptions return async (options, callback) => { headers[HEADER_USER_AGENT] = USER_AGENT const { uri } = options const target = urlJoin(baseUrl, uri) let opts if (options.formData) { opts = {} opts.method = method const form = buildFormData(options.formData) opts.body = form opts.headers = { ...headers, ...options.headers, ...form.getHeaders() } } else if (json) { opts = { ...options } opts.method = method opts.headers = { ...headers, ...options.headers } opts.headers[HEADER_CONTENT_TYPE] = MIME_JSON opts.body = JSON.stringify(opts.body) } else { opts = {} } const forceBuffer = opts.encoding === null try { const response = await fetch(target, opts) const responseBody = method === 'HEAD' ? undefined : await _parseResponseBody(response, forceBuffer) response.statusCode = response.status callback(null, response, responseBody) } catch (error) { callback(error) } } } /** * Fetch wrapper. Should mimic previous use of request(options, callback). * * Options are: * url: * (Description in request) * Fully qualified uri or a parsed url object from url.parse(). * * Value is created from Api options: 'https', 'host', 'port', and 'path'. * * qs: * (Description in request) * Object containing querystring values to be appended to the uri. * * Value from Api option 'query'. Default is {}. * * qsStringifyOptions: * (Description in request) * Object containing options to pass to the qs.stringify method. * Alternatively pass options to the querystring.stringify method using this format {sep:';', eq:':', options:{}}. * For example, to change the way arrays are converted to query strings using the qs module * pass the arrayFormat option with one of indices|brackets|repeat * * Value from Api option 'qsOptions' Default is { arrayFormat: 'brackets' }. Deprecated. * * method: * (Description in request) * http method (default: "GET") * * Value from Api option 'method'. Default is 'GET'. * * json: * (Description in request) * Sets body to JSON representation of value and adds Content-type: application/json header. * Additionally, parses the response body as JSON. * * Value from Api option 'json'. Default value is true. * * body: * (Description in request) * Entity body for PATCH, POST and PUT requests. Must be a Buffer, String or ReadStream. * If json is true, then body must be a JSON-serializable object. * * Value from Api option 'data', through JSON.stringify. * * headers: * (Description in request) * http headers (default: {}) * * Value from Api option 'headers'. Default value is {}. * * encoding: * (Description in request) * Encoding to be used on setEncoding of response data. If null, the body is returned as a Buffer. * Anything else (including the default value of undefined) will be passed as the encoding parameter to toString() (meaning this is effectively utf8 by default). * (Note: if you expect binary data, you should set encoding: null.) * * Value from Api option 'encoding'. Default value is 'utf8'. Deprecated. * * @param {object} options Options formerly passed to request * @param {function} callback Callback formerly passed to request */ async function fetchWrapper(options, callback) { const { url, qs = {}, method, json, body, headers } = options headers[HEADER_USER_AGENT] = USER_AGENT const queryObj = removeUndefined(qs) if (json) { headers[HEADER_CONTENT_TYPE] = MIME_JSON } const queryStr = new URLSearchParams(queryObj).toString() const fetchUrl = queryStr ? urlJoin(url, '?' + queryStr) : url const fetchOptions = { method, headers, body, } const forceBuffer = options.encoding === null try { const response = await fetch(fetchUrl, fetchOptions) const responseBody = await _parseResponseBody(response, forceBuffer) response.statusCode = response.status callback(null, response, responseBody) } catch (error) { callback(error) } } /** * Fetch wrappers with provided options. Should mimic previous use of request.defaults(options). * * Wrapper options are: * baseUrl: Used to prefix all calls * headers: Passed on to call header * json: (from request) Sets body to JSON representation of value and adds Content-type: application/json header. Additionally, parses the response body as JSON. * pool: Not implemented. Since Node 12 maxSockets are Infinity. (See https://nodejs.org/dist/v0.12.0/docs/api/http.html#http_agent_maxsockets) * * @param {object} wrapperOptions Options formerly passed to request.defaults * @returns Wrapped fetch with options */ function fetchWrappers(wrapperOptions = {}) { return { get: _createFetchWrapper(wrapperOptions, 'GET'), post: _createFetchWrapper(wrapperOptions, 'POST'), put: _createFetchWrapper(wrapperOptions, 'PUT'), del: _createFetchWrapper(wrapperOptions, 'DELETE'), head: _createFetchWrapper(wrapperOptions, 'HEAD'), patch: _createFetchWrapper(wrapperOptions, 'PATCH'), } } module.exports = { fetchWrapper, fetchWrappers, removeUndefined, }