extensions/http_api/client.js

'use strict'

/**
 * The http client used by the http API extension.
 *
 * @module extensions/httpApi/client
 */

const _ = require('lodash')
const request = require('request').defaults({ json: true })
const { Cookie } = require('tough-cookie')

const BODY_TYPE_JSON = 'json'
const BODY_TYPE_FORM = 'form'

/**
 * Http Api Client extension.
 *
 * @class
 */
class HttpApiClient {
    constructor() {
        // REQUEST INFORMATION
        this.body = null
        this.bodyType = null
        this.headers = null
        this.query = null
        this.cookies = []
        this.cookieJar = null

        // RESPONSE INFORMATION
        this.response = null
        this.responseCookies = {}
    }

    /**
     * Resets the client.
     */
    reset() {
        this.body = null
        this.bodyType = null
        this.headers = null
        this.query = null

        this.cookies = []
        this.cookieJar = null

        this.response = null
        this.responseCookies = {}
    }

    /**
     * Sets request json body.
     *
     * @param {Object} payload
     */
    setJsonBody(payload) {
        this.bodyType = BODY_TYPE_JSON
        this.body = payload
    }

    /**
     * Sets request form body.
     *
     * @param {Object} payload
     */
    setFormBody(payload) {
        this.bodyType = BODY_TYPE_FORM
        this.body = payload
    }

    /**
     * Clears current request body
     */
    clearBody() {
        this.body = null
        this.bodyType = null
    }

    /**
     * Sets request query parameters.
     *
     * @param {Object} query
     */
    setQuery(query) {
        this.query = query
    }

    /**
     * Sets request headers.
     *
     * @param {Object} headers
     */
    setHeaders(headers) {
        this.headers = headers
    }

    /**
     * Sets a single request header.
     *
     * @param {string} key
     * @param {string} value
     */
    setHeader(key, value) {
        this.headers = this.headers || {}
        this.headers[key] = value
    }

    /**
     * Clears current request headers.
     */
    clearHeaders() {
        this.headers = null
    }

    /**
     * Enables cookie jar.
     */
    enableCookies() {
        if (this.cookieJar !== null) return

        this.cookieJar = request.jar()
        this.cookieJar._jar.rejectPublicSuffixes = false
    }

    /**
     * Disables cookie jar.
     */
    disableCookies() {
        this.cookieJar = null
    }

    /**
     * Sets a cookie.
     * It does not actually add the cookie to the cookie jar
     * because setting the cookie requires the request url,
     * which we only have when making the request.
     *
     * @param {string|Object} cookie - Cookie string or Object
     */
    setCookie(cookie) {
        if (!_.isPlainObject(cookie) && !_.isString(cookie)) {
            throw new TypeError(`"cookie" must be a string or a cookie object`)
        }

        this.enableCookies()
        this.cookies.push(cookie)
    }

    /**
     * Clears registered request cookies.
     * Be aware that it does not clear existing response cookies.
     */
    clearRequestCookies() {
        this.cookies = []
    }

    /**
     * Retrieves a cookie by its key.
     *
     * @param {string} key - Cookie key
     * @return {Object|null} The cookie object if any, or null
     */
    getCookie(key) {
        if (this.responseCookies === null) return null
        if (this.responseCookies[key] === undefined) return null

        return this.responseCookies[key]
    }

    /**
     * Returns current response cookies.
     *
     * @return {Object} current response cookies
     */
    getCookies() {
        return this.responseCookies
    }

    /**
     * Returns the latest collected response.
     */
    getResponse() {
        return this.response
    }

    /**
     * Performs a request using all previously defined paramaters:
     * - headers
     * - query
     * - body
     *
     * @param {string} method    - The http verb
     * @param {string} path      - The path
     * @param {string} [baseUrl] - The base url
     */
    makeRequest(method, path, baseUrl) {
        return new Promise((resolve, reject) => {
            const options = {
                baseUrl: baseUrl,
                uri: path,
                method,
                qs: this.query || {},
                headers: this.headers,
                jar: this.cookieJar
            }

            const fullUri = `${baseUrl}${path}`

            if (this.body !== null) {
                if (!['POST', 'PUT'].includes(method)) {
                    throw new Error(
                        `You can only provides a body for POST and PUT HTTP methods, found: ${method}`
                    )
                }

                if (this.bodyType === BODY_TYPE_JSON) {
                    options.json = true
                    options.body = this.body
                } else if (this.bodyType === BODY_TYPE_FORM) {
                    options.form = this.body
                }
            }

            if (this.cookieJar !== null) {
                this.cookies.forEach(cookie => {
                    if (_.isPlainObject(cookie)) {
                        this.cookieJar.setCookie(new Cookie(cookie), fullUri)
                    } else if (_.isString(cookie)) {
                        this.cookieJar.setCookie(cookie, fullUri)
                    }
                })
            }

            request(options, (_error, _response, _body) => {
                if (_error) {
                    console.error(_error, options) // eslint-disable-line no-console
                    reject()
                }

                this.response = _response

                if (this.cookieJar !== null) {
                    this.responseCookies = {}
                    this.cookieJar.getCookies(fullUri).forEach(cookie => {
                        this.responseCookies[cookie.key] = cookie
                    })
                }

                resolve()
            })
        })
    }
}

/**
 * Create a new isolated http api client
 * @return {HttpApiClient}
 */
module.exports = function(...args) {
    return new HttpApiClient(...args)
}

/**
 * Http api client extension.
 * @type {HttpApiClient}
 */
module.exports.HttpApiClient = HttpApiClient