Source: ApiClient.js

/* * *  *  * *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * */
/* Copyright (c) 2020 Mobify Research & Development Inc. All rights reserved. */
/* * *  *  * *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  * */

/**
 * Shop API
 * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
 *
 * OpenAPI spec version: 20.4
 *
 *
 * NOTE: This class is auto generated by the swagger code generator program.
 * https://github.com/swagger-api/swagger-codegen.git
 * Do not edit the class manually.
 *
 */
import superagent from 'superagent'
import querystring from 'querystring'

import Fault from './models/Fault'

/**
 * @module ApiClient
 * @version 20.4
 */

const defaultConfig = {
    basePath: 'https://localhost/s/siteId/dw/shop/v20_4',
    cache: true,
    defaultHeaders: {},
    enableCookies: false,
    overrideHttpPut: true,
    timeout: 60000,
}

/**
 * Manages low level client-server communications, parameter marshalling, etc. There should not be any need for an
 * application to use this class directly - the *Api and model classes provide the public API for the service. The
 * contents of this file should be regarded as internal but are documented for completeness.
 * @alias module:ApiClient
 * @class
 */
export default class ApiClient {

    constructor(config = defaultConfig) {
        const {
            basePath,
            defaultHeaders,
            timeout,
            cache,
            enableCookies,
            clientUsername,
            clientPassword,
            oauth2AccessToken,
            overrideHttpPut
        } = Object.assign(defaultConfig, config)

        // verify the required parameter 'basepath' is set
        if (basePath === undefined || basePath === null || basePath === '') {
            throw new Error('Missing the required parameter \'basePath\' when calling constructing ApiClient')
        }

        /**
         * The base URL against which to resolve every API call's (relative) path.
         * @type {String}
         * @default https://localhost/s/siteId/dw/shop/v20_4
         */
        this.basePath = basePath.replace(/\/+$/, '')

        /**
         * The authentication methods to be included for all API calls.
         * @type {Array.<String>}
         */
        this.authentications = {
            client_id: {
                type: 'apiKey',
                in: 'header',
                name: 'x-dw-client-id'
            },
            customers_auth: {
                type: 'basic'
            },
            oauth2_application: {
                type: 'oauth2'
            }
        }

        if (oauth2AccessToken) {
            const oauth2_application = this.authentications.oauth2_application
            oauth2_application.accessToken = oauth2AccessToken
        }

        if (clientUsername && clientPassword) {
            const customers_auth = this.authentications.customers_auth
            customers_auth.username = clientUsername
            customers_auth.password = clientPassword
        }

        /**
         * If set to true, endpoints that normally use HTTP `PUT` will
         * be sent using `POST` with an aditional header (x-dw-http-method-override: `PUT`).
         * Please refer to the following Salesforce documentation {@link https://documentation.demandware.com/DOC1/topic/com.demandware.dochelp/OCAPI/18.8/usage/HttpMethods.html}
         * for more information.
         * @type {Boolean}
         * @default true
         */
        this.overrideHttpPut = overrideHttpPut

        /**
         * The default HTTP headers to be included for all API calls.
         * @type {Array.<String>}
         * @default {}
         */
        this.defaultHeaders = defaultHeaders

        /**
         * The default HTTP timeout for all API calls.
         * @type {Number}
         * @default 60000
         */
        this.timeout = timeout

        /**
         * If set to false an additional timestamp parameter is added to all API GET calls to
         * prevent browser caching
         * @type {Boolean}
         * @default true
         */
        this.cache = cache

        /**
         * If set to true, the client will save the cookies from each server
         * response, and return them in the next request.
         * @default false
         */
        this.enableCookies = enableCookies

        /*
         * Used to save and return cookies in a node.js (non-browser) setting,
         * if this.enableCookies is set to true.
         */
        if (typeof window === 'undefined') {
            this.agent = new superagent.agent()
        }
    }

    /**
     * Returns a string representation for an actual parameter.
     * @param param The actual parameter.
     * @returns {String} The string representation of <code>param</code>.
     */
    paramToString(param) {
        if (param === undefined || param === null) {
            return ''
        }
        if (param instanceof Date) {
            return param.toJSON()
        }

        return param.toString()
    }

    /**
     * Returns a encoded string representation for an actual parameter.
     * @param param The actual parameter.
     * @returns {String} The string representation of <code>param</code>.
     */
    paramToEncodedString(param) {
        let value = ''

        if (param === undefined || param === null) {
            return value
        }

        if (param instanceof Date) {
            value = encodeURIComponent(param.toJSON())
        } else if (param instanceof Array) {
            value = param.map((value) => encodeURIComponent(value)).join(',')
        } else {
            value = encodeURIComponent(param.toString())
        }

        return value
    }

    /**
     * Builds full URL by appending the given path to the base URL and replacing path parameter place-holders with parameter values.
     * NOTE: query parameters are not handled here.
     * @param {String} path The path to append to the base URL.
     * @param {Object} pathParams The parameter values to append.
     * @returns {String} The encoded path with parameter values substituted.
     */
    buildUrl(path, pathParams) {
        if (!path.match(/^\//)) {
            path = `/${path}`
        }

        let url = this.basePath + path
        url = url.replace(/\{([\w-]+)\}/g, (fullMatch, key) => {
            let value
            if (pathParams.hasOwnProperty(key)) {
                value = this.paramToEncodedString(pathParams[key])
            } else {
                value = encodeURIComponent(fullMatch)
            }

            return value
        })

        return url
    }

    /**
     * Checks whether the given content type represents JSON.<br>
     * JSON content type examples:<br>
     * <ul>
     * <li>application/json</li>
     * <li>application/json; charset=UTF8</li>
     * <li>APPLICATION/JSON</li>
     * </ul>
     * @param {String} contentType The MIME content type to check.
     * @returns {Boolean} <code>true</code> if <code>contentType</code> represents JSON, otherwise <code>false</code>.
     */
    isJsonMime(contentType) {
        return Boolean(contentType !== null && contentType.match(/^application\/json(;.*)?$/i))
    }

    /**
     * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first.
     * @param {Array.<String>} contentTypes
     * @returns {String} The chosen content type, preferring JSON.
     */
    jsonPreferredMime(contentTypes) {
        for (let i = 0; i < contentTypes.length; i++) {
            if (this.isJsonMime(contentTypes[i])) {
                return contentTypes[i]
            }
        }

        return contentTypes[0]
    }

    /**
     * Checks whether the given parameter value represents file-like content.
     * @param param The parameter to check.
     * @returns {Boolean} <code>true</code> if <code>param</code> represents a file.
     */
    isFileParam(param) {
        // fs.ReadStream in Node.js and Electron (but not in runtime like browserify)
        if (typeof require === 'function') {
            let fs
            try {
                fs = require('fs')
            } catch (err) {}
            if (fs && fs.ReadStream && param instanceof fs.ReadStream) {
                return true
            }
        }

        // Buffer in Node.js
        if (typeof Buffer === 'function' && param instanceof Buffer) { // eslint-disable-line no-undef
            return true
        }

        // Blob in browser
        if (typeof Blob === 'function' && param instanceof Blob) {
            return true
        }

        // File in browser (it seems File object is also instance of Blob, but keep this for safe)
        if (typeof File === 'function' && param instanceof File) {
            return true
        }

        return false
    }

    /**
     * Normalizes parameter values:
     * <ul>
     * <li>remove nils</li>
     * <li>keep files and arrays</li>
     * <li>format to string with `paramToString` for other cases</li>
     * </ul>
     * @param {Object.<String, Object>} params The parameters as object properties.
     * @returns {Object.<String, Object>} normalized parameters.
     */
    normalizeParams(params) {
        const newParams = {}
        for (const key in params) {
            if (params.hasOwnProperty(key) && params[key] !== undefined && params[key] !== null) {
                const value = params[key]
                if (this.isFileParam(value) || Array.isArray(value)) {
                    newParams[key] = value
                } else {
                    newParams[key] = this.paramToString(value)
                }
            }
        }

        return newParams
    }

    /**
     * Builds an object with refinement keys 1..n given a an array of refinements.
     * A numbered suffix will not be applied if the number of refinements is equal to 1.
     * @param {Array} refinements Array of refinement strings
     * @returns {Object} An object with refinement keys numbered 1 ... n with their
     * string representation value.
     */
    buildRefineParams(refinements) {
        refinements = refinements || []

        return refinements.length
            ? refinements.reduce(
                (acc, curr, idx, arr) => {
                    Object.assign(acc, {[arr.length > 1 ? `refine_${idx + 1}` : 'refine']: this.paramToString(curr)})
                    return acc
                },
                {} // Reduce array to populate a new object with formatted key/values.
            )
            : {}
    }

    /**
     * Builds a string representation of an array-type actual parameter, according to the given collection format.
     * @param {Array} param An array parameter.
     * @param {module:ApiClient.CollectionFormatEnum} collectionFormat The array element separator strategy.
     * @returns {String|Array} A string representation of the supplied collection, using the specified delimiter. Returns
     * <code>param</code> as is if <code>collectionFormat</code> is <code>multi</code>.
     */
    buildCollectionParam(param, collectionFormat) {
        if (param === null || param === undefined) {
            return null
        }

        switch (collectionFormat) {
            case 'csv':
                return param.map(this.paramToString).join(',')
            case 'ssv':
                return param.map(this.paramToString).join(' ')
            case 'tsv':
                return param.map(this.paramToString).join('\t')
            case 'pipes':
                return param.map(this.paramToString).join('|')
            case 'multi':
                // return the array directly as SuperAgent will handle it as expected
                return param.map(this.paramToString)
            default:
                throw new Error(`Unknown collection format: ${collectionFormat}`)
        }
    }

    /**
     * Applies authentication headers to the request.
     * @param {Object} request The request object created by a <code>superagent()</code> call.
     * @param {Array.<String>} authNames An array of authentication method names.
     */
    applyAuthToRequest(request, authNames) {
        authNames.forEach((authName) => {
            const auth = this.authentications[authName]
            switch (auth.type) {
                case 'basic':
                    if (auth.username || auth.password) {
                        request.auth(auth.username || '', auth.password || '')
                    }

                    break
                case 'apiKey':
                    if (auth.apiKey) {
                        const data = {}
                        if (auth.apiKeyPrefix) {
                            data[auth.name] = `${auth.apiKeyPrefix} ${auth.apiKey}`
                        } else {
                            data[auth.name] = auth.apiKey
                        }

                        if (auth.in === 'header') {
                            request.set(data)
                        } else {
                            request.query(data)
                        }
                    }

                    break
                case 'oauth2':
                    if (auth.accessToken) {
                        request.set({
                            Authorization: `Bearer ${auth.accessToken}`
                        })
                    }

                    break
                default:
                    throw new Error(`Unknown authentication type: ${auth.type}`)
            }
        })
    }

    /**
     * Deserializes an HTTP response body into a value of the specified type.
     * @param {Object} response A SuperAgent response object.
     * @param {(String|Array.<String>|Object.<String, Object>|Function)} returnType The type to return. Pass a string for simple types
     * or the constructor function for a complex type. Pass an array containing the type name to return an array of that type. To
     * return an object, pass an object with one property whose name is the key type and whose value is the corresponding value type:
     * all properties on <code>data<code> will be converted to this type.
     * @returns A value of the specified type.
     */
    deserialize(response, returnType) {
        if (response === null || returnType === null || response.status === 204) {
            return null
        }

        // Rely on SuperAgent for parsing response body.
        // See http://visionmedia.github.io/superagent/#parsing-response-bodies
        let data = response.body
        if (data === null || (typeof data === 'object' && typeof data.length === 'undefined' && !Object.keys(data).length)) {
            // SuperAgent does not always produce a body; use the unparsed response as a fallback
            data = response.text
        }

        return ApiClient.convertToType(data, returnType)
    }

    /**
     * Invokes the REST service using the supplied settings and parameters.
     * @param {String} path The base URL to invoke.
     * @param {String} httpMethod The HTTP method to use.
     * @param {Object.<String, String>} pathParams A map of path parameters and their values.
     * @param {Object.<String, Object>} queryParams A map of query parameters and their values.
     * @param {Object.<String, Object>} headerParams A map of header parameters and their values.
     * @param {Object.<String, Object>} formParams A map of form parameters and their values.
     * @param {Object} bodyParam The value to pass as the request body.
     * @param {Array.<String>} authNames An array of authentication type names.
     * @param {Array.<String>} contentTypes An array of request MIME types.
     * @param {Array.<String>} accepts An array of acceptable response MIME types.
     * @param {(String|Array|ObjectFunction)} returnType The required type to return; can be a string for simple types or the
     * constructor for a complex type.
     * @returns {Promise} A {@link https://www.promisejs.org/|Promise} object.
     */
    callApi(path, httpMethod, pathParams,
        queryParams, headerParams, formParams, bodyParam, authNames, contentTypes, accepts,
        returnType) {

        // emulate PUT method because they are not allowed on staging and production environments
        if (this.overrideHttpPut && httpMethod.toUpperCase() === 'PUT') {
            httpMethod = 'POST'
            headerParams = Object.assign(headerParams || {}, {'x-dw-http-method-override': 'PUT'})
        }

        const url = this.buildUrl(path, pathParams)
        const request = superagent(httpMethod, url)

        // apply authentications
        this.applyAuthToRequest(request, authNames)

        // set query parameters
        if (httpMethod.toUpperCase() === 'GET' && this.cache === false) {
            queryParams._ = new Date().getTime()
        }

        request.query(this.normalizeParams(queryParams))

        // set header parameters
        request.set(this.defaultHeaders).set(this.normalizeParams(headerParams))

        // set request timeout
        request.timeout(this.timeout)

        const contentType = this.jsonPreferredMime(contentTypes)
        if (contentType) {
            // Issue with superagent and multipart/form-data (https://github.com/visionmedia/superagent/issues/746)
            if (contentType !== 'multipart/form-data') {
                request.type(contentType)
            }
        } else if (!request.header['Content-Type']) {
            request.type('application/json')
        }

        if (contentType === 'application/x-www-form-urlencoded') {
            request.send(querystring.stringify(this.normalizeParams(formParams)))
        } else if (contentType === 'multipart/form-data') {
            const _formParams = this.normalizeParams(formParams)
            for (const key in _formParams) {
                if (_formParams.hasOwnProperty(key)) {
                    if (this.isFileParam(_formParams[key])) {
                        // file field
                        request.attach(key, _formParams[key])
                    } else {
                        request.field(key, _formParams[key])
                    }
                }
            }
        } else if (bodyParam) {
            request.send(bodyParam)
        }

        const accept = this.jsonPreferredMime(accepts)
        if (accept) {
            request.accept(accept)
        }

        if (returnType === 'Blob') {
            request.responseType('blob')
        } else if (returnType === 'String') {
            request.responseType('string')
        }

        // Attach previously saved cookies, if enabled
        if (this.enableCookies) {
            if (typeof window === 'undefined') {
                this.agent._attachCookies(request)
            } else {
                request.withCredentials()
            }
        }

        return this.sendApiRequest(request, returnType)
    }

    /**
     * Sends the generated superagent request and deserializes the response.
     * @param {module:superagent.Request} request The superagent request to send.
     * @param {String} returnType The type to deserialize the response into.
     * @returns {Promise} A {@link https://www.promisejs.org/|Promise} object.
     */
    sendApiRequest(request, returnType) {
        return new Promise((resolve, reject) => {
            request.end((error, response) => {
                if (error) {

                    // Looks like there was an fault returned from the API
                    const hasErrorMessage = error.response && error.response.text
                    if (hasErrorMessage) {
                        try {
                            const fault = Fault.constructFromObject(JSON.parse(error.response.text).fault)
                            reject(fault)
                        } catch (err) {
                            // Reject immediately on parsing error.
                            reject(err)
                        }
                    }

                    // Most likely a network error has happened here so include entire error.
                    reject(error)
                } else {
                    try {
                        const data = this.deserialize(response, returnType)
                        if (this.enableCookies && typeof window === 'undefined') {
                            this.agent._saveCookies(response)
                        }

                        resolve({
                            data,
                            response
                        })
                    } catch (err) {
                        reject(err)
                    }
                }
            })
        })
    }

    /**
     * Parses an ISO-8601 string representation of a date value.
     * @param {String} str The date value as a string.
     * @returns {Date} The parsed date object.
     */
    static parseDate(str) {
        return new Date(str.replace(/T/i, ' '))
    }

    /**
     * Converts a value to the specified type.
     * @param {(String|Object)} data The data to convert, as a string or object.
     * @param {(String|Array.<String>|Object.<String, Object>|Function)} type The type to return. Pass a string for simple types
     * or the constructor function for a complex type. Pass an array containing the type name to return an array of that type. To
     * return an object, pass an object with one property whose name is the key type and whose value is the corresponding value type:
     * all properties on <code>data<code> will be converted to this type.
     * @returns An instance of the specified type or null or undefined if data is null or undefined.
     */
    static convertToType(data, type) {
        if (data === null || data === undefined) {
            return data
        }

        switch (type) {
            case 'Boolean':
                return Boolean(data)
            case 'Integer':
                return parseInt(data, 10)
            case 'Number':
                return parseFloat(data)
            case 'String':
                return String(data)
            case 'Date':
                return ApiClient.parseDate(String(data))
            case 'Blob':
                return data
            default:
                if (type === Object) {
                    // generic object, return directly
                    return data
                } else if (typeof type === 'function') {
                    // for model type like: User
                    const model = type.constructFromObject(data)

                    // add support for custom properties
                    // NOTE: We'll have to expand on this further as this only suports
                    // simple typed values
                    for (const k in data) {
                        if (data.hasOwnProperty(k) && /^c_/.test(k)) {
                            model[k] = data[k]
                        }
                    }

                    return model
                } else if (Array.isArray(type)) {
                    // for array type like: ['String']
                    const itemType = type[0]

                    return data.map((item) => {
                        return ApiClient.convertToType(item, itemType)
                    })
                } else if (typeof type === 'object') {
                    // for plain object type like: {'String': 'Integer'}
                    let keyType
                    let valueType
                    for (const k in type) {
                        if (type.hasOwnProperty(k)) {
                            keyType = k
                            valueType = type[k]
                            break
                        }
                    }

                    const result = {}
                    for (const k in data) {
                        if (data.hasOwnProperty(k)) {
                            const key = ApiClient.convertToType(k, keyType)
                            const value = ApiClient.convertToType(data[k], valueType)
                            result[key] = value
                        }
                    }

                    return result
                } else {
                    // for unknown type, return the data directly
                    return data
                }
        }
    }

    /**
     * Constructs a new map or array model from REST data.
     * @param data {Object|Array} The REST data.
     * @param obj {Object|Array} The target object or array.
     */
    static constructFromObject(data, obj, itemType) {
        if (Array.isArray(data)) {
            for (let i = 0; i < data.length; i++) {
                if (data.hasOwnProperty(i)) {
                    obj[i] = ApiClient.convertToType(data[i], itemType)
                }
            }
        } else {
            for (const k in data) {
                if (data.hasOwnProperty(k)) {
                    obj[k] = ApiClient.convertToType(data[k], itemType)
                }
            }
        }
    }
}

/**
 * Enumeration of collection format separator strategies.
 * @enum {String}
 * @readonly
 */
ApiClient.CollectionFormatEnum = {
    /**
     * Comma-separated values. Value: <code>csv</code>
     * @const
     */
    CSV: ',',

    /**
     * Space-separated values. Value: <code>ssv</code>
     * @const
     */
    SSV: ' ',

    /**
     * Tab-separated values. Value: <code>tsv</code>
     * @const
     */
    TSV: '\t',

    /**
     * Pipe(|)-separated values. Value: <code>pipes</code>
     * @const
     */
    PIPES: '|',

    /**
     * Native array. Value: <code>multi</code>
     * @const
     */
    MULTI: 'multi'
}

/**
 * The default API client implementation.
 * @type {module:ApiClient}
 */
ApiClient.instance = new ApiClient(defaultConfig)