UNPKG

@m92/express-utils

Version:

Utility Module for Express Framework

411 lines (337 loc) 12.7 kB
'use strict' import axios from 'axios' import axiosRetry from 'axios-retry' import context from 'express-http-context' import { transform } from 'camaro' import { nanoid } from 'nanoid' import mime from 'mime-types' import autoBind from 'auto-bind' import HTTP_CLIENT_CONFIG from '../defaults/HTTP_CLIENT_CONFIG' import AXIOS_RETRY_CONFIG from '../defaults/AXIOS_RETRY_CONFIG' export default function HttpClientCreator (CONFIG, CONSTANTS, thisExpressUtils) { const { HTTP_CLIENT_LOG_ENABLED, HTTP_CLIENT_BODY_LOG_ENABLED, AAUTH_API_KEY } = CONFIG const { UID_CONTEXT, UID_HEADER_KEY, SESSION_ID_CONTEXT, SESSION_ID_HEADER_KEY, REQUEST_ID_CONTEXT, REQUEST_ID_HEADER_KEY, API_KEY_HEADER_KEY, DEFAULT_SOAP_VERSION } = CONSTANTS const { CustomError, logger, ERROR_CLASSIFICATIONS } = thisExpressUtils return class HttpClient { constructor (config = {}, overrideHeaders = {}) { const uid = overrideHeaders.uid || context.get(UID_CONTEXT) || '' const sessionId = overrideHeaders.sessionId || context.get(SESSION_ID_CONTEXT) || '' const requestId = overrideHeaders.requestId || context.get(REQUEST_ID_CONTEXT) || '' const headers = { [UID_HEADER_KEY]: uid, [SESSION_ID_HEADER_KEY]: sessionId, [REQUEST_ID_HEADER_KEY]: requestId } const { disableBodyLogging } = config const customConfig = { disableBodyLogging: !!disableBodyLogging, overrideHeaders } // retry config const { retry: retryConfig = {}, ...axiosConfig } = config const axiosConf = { ...HTTP_CLIENT_CONFIG, headers, ...axiosConfig } this.client = axios.create(axiosConf) // Request Interceptors this.client.interceptors.request.use( // Interceptor for Request Success HttpClient.requestLoggerSuccess(customConfig), // Interceptor for Request Failure HttpClient.requestLoggerFailure(customConfig) ) // Response Interceptors this.client.interceptors.response.use( // Interceptor for Response Success HttpClient.responseLoggerSuccess(customConfig), // Interceptor for Response Failure HttpClient.responseLoggerFailure(customConfig) ) // axios-retry const axiosRetryConfig = { ...AXIOS_RETRY_CONFIG, ...retryConfig } axiosRetry(this.client, axiosRetryConfig) autoBind(this) } async request (options, errorMap) { const { client } = this const { method, url } = options const classification = ERROR_CLASSIFICATIONS.API_CALL if (!method) { const methodErrorMsg = 'HttpClientError: \'method\' cannot be empty' const methodErrorParams = { statusCode: 400, message: methodErrorMsg, classification } throw new CustomError(undefined, methodErrorParams) } if (!url) { const urlErrorMsg = 'HttpClientError: \'url\' cannot be empty' const urlErrorParams = { statusCode: 400, message: urlErrorMsg, classification } throw new CustomError(undefined, urlErrorParams) } const sanitizedOptions = this.sanitizeOptions(options) return client.request(sanitizedOptions).catch(error => HttpClient.throwError(error, errorMap)) } async requestSoap (options, errorMap) { const basicHeaders = { 'Content-Type': 'application/xml' } const requestOptions = { ...options, headers: { ...basicHeaders, ...options.headers } } const { method, url } = requestOptions const classification = ERROR_CLASSIFICATIONS.API_CALL if (!method) { const methodErrorMsg = 'HttpClientError: \'method\' cannot be empty' const methodErrorParams = { statusCode: 400, message: methodErrorMsg, classification } throw new CustomError(undefined, methodErrorParams) } if (!url) { const urlErrorMsg = 'HttpClientError: \'url\' cannot be empty' const urlErrorParams = { statusCode: 400, message: urlErrorMsg, classification } throw new CustomError(undefined, urlErrorParams) } return this.client.request(requestOptions) .catch(error => HttpClient.throwError(error)) .then(response => HttpClient.throwSoapError(response, errorMap)) } async requestSoapWithAttachment (options, attachment = {}, errorMap) { const { headers: optHeader = {}, data: optData = '', soapVersion = DEFAULT_SOAP_VERSION } = options const { headers, data } = await this._addAttachment(soapVersion, optData, attachment) const requestHeaders = { ...headers, ...optHeader } const requestOptions = { ...options, headers: requestHeaders, data } return this.requestSoap(requestOptions, errorMap) } sanitizeOptions (options) { const { headers, disableApiKey = false } = options // Handle Header for API Key const apiKeyHeader = { [API_KEY_HEADER_KEY]: AAUTH_API_KEY } const sanitizedHeaders = { ...(!disableApiKey && apiKeyHeader), ...headers } const sanitizedOptions = { ...options, headers: sanitizedHeaders } return sanitizedOptions } async _addAttachment (soapVersion, soapData, attachment) { const { file, fileName, type } = attachment const mimeType = attachment.mimeType || this._getMimeType(fileName) const { contentType, attachmentEncoding } = this._getSoapVersionBasedProps(soapVersion) const fileString = await this._getFile(file, type, attachmentEncoding) const boundary = `----=_Part_1_${nanoid()}` const startContentId = `<rootpart.${nanoid()}>` const headers = { 'Content-Type': `multipart/related; type="${contentType}"; start="${startContentId}"; boundary="${boundary}"`, SoapAction: '' } // Code is indent in such fashion due to the use of string interpolation const startBoundary = `--${boundary} Content-Type: ${contentType}; charset=UTF-8 Content-Transfer-Encoding: 8bit Content-ID: ${startContentId} ` const attachmentData = `--${boundary} Content-Type: ${mimeType}; name=${fileName} Content-Transfer-Encoding: ${attachmentEncoding} Content-ID: <${fileName}-${nanoid()}> ${fileString}` const endBoundary = `--${boundary}--` const data = `${startBoundary} ${soapData} ${attachmentData} ${endBoundary}` return { headers, data } } _getSoapVersionBasedProps (soapVersion) { switch (soapVersion) { case '1.1': return { contentType: 'text/xml', attachmentEncoding: 'base64' } case '1.2': return { contentType: 'application/xop+xml', attachmentEncoding: 'base64' } } } _getMimeType (fileName) { return mime.lookup(fileName) } async _getFile (file, type, attachmentEncoding) { switch (type) { case 'buffer': { return file.toString(attachmentEncoding) } case 'base64': { return (attachmentEncoding === 'base64' && file) || Buffer.from(file, 'base64').toString(attachmentEncoding) } case 'url': { const requestOptions = { method: 'GET', url: file, responseType: 'arraybuffer' } try { const response = await this.request(requestOptions) const { data: body } = response return (attachmentEncoding === 'binary' && body) || Buffer.from(body, 'binary').toString(attachmentEncoding) } catch (e) { const message = `Unable to download file from URL: ${file}` const classification = ERROR_CLASSIFICATIONS.API_CALL const errorParams = { message, classification } const error = Buffer.from(e.error, 'binary').toString('utf8') throw new CustomError({ ...e, error }, errorParams) } } default: { const message = `Invalid Soap Attachment Type: ${type}` const classification = ERROR_CLASSIFICATIONS.VALIDATION const errorParams = { statusCode: 500, message, classification } throw new CustomError(null, errorParams) } } } static async throwError (error, errorMap = {}) { const { request, response } = error // Handle Axios Response Error if (response) { const { status = 500, data: body } = response const { statusCode, message, data, error } = body const statusErrorMap = errorMap[status] || {} const classification = ERROR_CLASSIFICATIONS.API_CALL const { name, code, message: errMapMsg, statusCode: errMapStatusCode } = statusErrorMap const e = (error || ((statusCode && message && data && undefined) || body)) const errorParams = { statusCode: (errMapStatusCode || statusCode || status), message: (errMapMsg || message || undefined), classification, code, name, data } const err = new CustomError(e, errorParams, false) throw err } // Handle Axios Request Error if (request) { const classification = ERROR_CLASSIFICATIONS.CONNECTION const { message } = error const errorParams = { statusCode: 500, message, classification } const err = new CustomError(error, errorParams, false) delete err.error.stack throw err } // Handle any other form of error const err = new CustomError(error) throw err } static async throwSoapError (response, errorMap) { if (!errorMap) { return response } const { statusKey = '', messageKey = '', statusValueMap = {} } = errorMap const errorTemplate = { status: statusKey, message: messageKey } const error = await transform(response, errorTemplate) const mappedError = statusValueMap[error.status] if (!mappedError) { return response } const { statusCode, message } = mappedError const classification = ERROR_CLASSIFICATIONS.API_CALL const errorParams = { statusCode, message, classification } throw new CustomError(error, errorParams, false) } static requestLoggerSuccess (customConfig) { return function (config) { const isError = false HttpClient.logRequest(config, isError, customConfig) return config } } static requestLoggerFailure (customConfig) { return function (error) { const isError = true HttpClient.logRequest(error, isError, customConfig) return Promise.reject(error) } } static responseLoggerSuccess (customConfig) { return function (response) { const isError = false HttpClient.logResponse(response, isError, customConfig) return response } } static responseLoggerFailure (customConfig) { return function (error) { const isError = true HttpClient.logResponse(error, isError, customConfig) return Promise.reject(error) } } static logRequest (config, isError, customConfig) { if (!HTTP_CLIENT_LOG_ENABLED) { return } const { config: axiosConfig } = config const { method, url, data: body } = axiosConfig || config const { disableBodyLogging, overrideHeaders = {} } = customConfig const logMsg = `HttpClientRequest | ${method} ${url}` const logData = { url, method, body: ((HTTP_CLIENT_BODY_LOG_ENABLED && !disableBodyLogging && body) || {}), ...overrideHeaders } const logFunction = isError ? logger.error : logger.trace logFunction(logMsg, logData) } static logResponse (response, isError, customConfig) { if (!HTTP_CLIENT_LOG_ENABLED) { return } const { config = {}, code = '', response: axiosResponse } = response const { status = 500, statusText = '', data: body } = axiosResponse || response const { method = '', url = '' } = config const { disableBodyLogging, overrideHeaders = {} } = customConfig const logMsg = `HttpClientResponse | ${method} ${url} | ${status} ${code || statusText}` const logData = { url, method, code, status, statusText, body: ((HTTP_CLIENT_BODY_LOG_ENABLED && !disableBodyLogging && body) || {}), ...overrideHeaders } const logFunction = isError ? logger.error : logger.info logFunction(logMsg, logData) } } }