Home Reference Source Test

src/utils/ApiClient.js

import request from "superagent";
import { v4 as uuidv4 } from "uuid";
import packageJson from "../../package.json";
import Encryption from "./Encryption";
import HyperwalletVerificationDocument from "../models/HyperwalletVerificationDocument";
import HyperwalletVerificationDocumentReason from "../models/HyperwalletVerificationDocumentReason";

/**
 * The callback interface for api calls
 *
 * @typedef {function} api-callback
 * @param {Object[]} [errors] - In case of an error an array with error objects otherwise undefined
 * @param {string} [errors[].fieldName] - The field name (if error is caused by a particular field)
 * @param {string} errors[].message - The error message
 * @param {string} errors[].code - The error code
 * @param {Object} data - The rest response body
 * @param {Object} res - The raw superagent response object
 */

/**
 * The Hyperwallet API Client
 */
export default class ApiClient {
    /**
     * Create a instance of the API client
     *
     * @param {string} username - The API username
     * @param {string} password - The API password
     * @param {string} server - The API server to connect to
     * @param {string} encryptionData - The API encryption data
     */
    constructor(username, password, server, encryptionData) {
        /**
         * The API username
         *
         * @type {string}
         * @protected
         */
        this.username = username;

        /**
         * The API password
         *
         * @type {string}
         * @protected
         */
        this.password = password;

        /**
         * The API server to connect to
         * @type {string}
         * @protected
         */
        this.server = server;

        /**
         * The Node SDK Version number
         *
         * @type {string}
         * @protected
         */
        this.version = packageJson.version;

        /**
         * The flag shows if encryption is enabled
         *
         * @type {boolean}
         * @protected
         */
        this.isEncrypted = false;
        this.contextId = uuidv4();
        if (encryptionData && encryptionData.clientPrivateKeySetPath && encryptionData.hyperwalletKeySetPath) {
            this.isEncrypted = true;
            this.clientPrivateKeySetPath = encryptionData.clientPrivateKeySetPath;
            this.hyperwalletKeySetPath = encryptionData.hyperwalletKeySetPath;
            this.encryption = new Encryption(this.clientPrivateKeySetPath, this.hyperwalletKeySetPath);
        }
    }

    /**
     * Format response to documents model before passing to callback
     *
     * @param {Object} res - Response object
     *
     */
    static formatResForCallback(res) {
        const retRes = res;

        if (res && res.body) {
            const retBody = res.body;
            const { documents } = retBody;
            if (documents && documents.length > 0) {
                const documentsArr = [];
                documents.forEach((dVal) => {
                    const doc = dVal;
                    if (dVal.reasons && dVal.reasons.length > 0) {
                        const reasonsArr = [];
                        dVal.reasons.forEach((rVal) => {
                            reasonsArr.push(new HyperwalletVerificationDocumentReason(rVal));
                        });
                        doc.reasons = reasonsArr;
                    }
                    documentsArr.push(new HyperwalletVerificationDocument(doc));
                });
                retBody.documents = documentsArr;
                retRes.body = retBody;
            }
        }
        return retRes;
    }

    /**
     * Do a POST call to the Hyperwallet API server
     *
     * @param {string} partialUrl - The api endpoint to call (gets prefixed by `server` and `/rest/v4/`)
     * @param {Object} data - The data to send to the server
     * @param {Object} params - Query parameters to send in this call
     * @param {api-callback} callback - The callback for this call
     */
    doPost(partialUrl, data, params, callback) {
        let contentType = "application/json";
        let accept = "application/json";
        let requestDataPromise = new Promise(resolve => resolve(data));
        if (this.isEncrypted) {
            contentType = "application/jose+json";
            accept = "application/jose+json";
            ApiClient.createJoseJsonParser();
            requestDataPromise = this.encryption.encrypt(data);
        }
        requestDataPromise.then((requestData) => {
            request
                .post(`${this.server}/rest/v4/${partialUrl}`)
                .auth(this.username, this.password)
                .set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
                .set("x-sdk-version", this.version)
                .set("x-sdk-type", "NodeJS")
                .set("x-sdk-contextId", this.contextId)
                .type(contentType)
                .accept(accept)
                .query(params)
                .send(requestData)
                .end(this.wrapCallback("POST", callback));
        }).catch(() => callback([{ message: "Failed to encrypt body for POST request" }], undefined, undefined));
    }

    /**
     * Do a PUT call to the Hyperwallet API server to upload documents
     *
     * @param {string} partialUrl - The api endpoint to call (gets prefixed by `server` and `/rest/v4/`)
     * @param {Object} data - The data to send to the server
     * @param {api-callback} callback - The callback for this call
     */
    doPutMultipart(partialUrl, data, callback) {
        let contentType = "multipart/form-data";
        let accept = "application/json";
        /* eslint-disable no-unused-vars */
        const keys = Object.keys(data);
        /* eslint-enable no-unused-vars */

        let requestDataPromise = new Promise(resolve => resolve(data));
        if (this.isEncrypted) {
            contentType = "multipart/form-data";
            accept = "application/jose+json";
            ApiClient.createJoseJsonParser();
            requestDataPromise = this.encryption.encrypt(data);
        }
        requestDataPromise.then(() => {
            const req = request
                .put(`${this.server}/rest/v4/${partialUrl}`)
                .auth(this.username, this.password)
                .set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
                .set("x-sdk-version", this.version)
                .set("x-sdk-type", "NodeJS")
                .set("x-sdk-contextId", this.contextId)
                .type(contentType)
                .accept(accept);
            keys.forEach((key) => {
                if (key === "data") {
                    req.field(key, JSON.stringify(data[key]));
                } else {
                    req.attach(key, data[key]);
                }
            });
            req.end(this.wrapCallback("PUT", callback));
        }).catch(err => callback(err, undefined, undefined));
    }

    /**
     * Do a PUT call to the Hyperwallet API server
     *
     * @param {string} partialUrl - The api endpoint to call (gets prefixed by server and /rest/v4/)
     * @param {Object} data - The data to send to the server
     * @param {Object} params - Query parameters to send in this call
     * @param {api-callback} callback - The callback for this call
     */
    doPut(partialUrl, data, params, callback) {
        let contentType = "application/json";
        let accept = "application/json";
        let requestDataPromise = new Promise(resolve => resolve(data));
        if (this.isEncrypted) {
            contentType = "application/jose+json";
            accept = "application/jose+json";
            ApiClient.createJoseJsonParser();
            requestDataPromise = this.encryption.encrypt(data);
        }
        requestDataPromise.then((requestData) => {
            request
                .put(`${this.server}/rest/v4/${partialUrl}`)
                .auth(this.username, this.password)
                .set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
                .set("x-sdk-version", this.version)
                .set("x-sdk-type", "NodeJS")
                .set("x-sdk-contextId", this.contextId)
                .type(contentType)
                .accept(accept)
                .query(params)
                .send(requestData)
                .end(this.wrapCallback("PUT", callback));
        }).catch(() => callback([{ message: "Failed to encrypt body for PUT request" }], undefined, undefined));
    }

    /**
     * Do a GET call to the Hyperwallet API server
     *
     * @param {string} partialUrl - The api endpoint to call (gets prefixed by `server` and `/rest/v4/`)
     * @param {Object} params - Query parameters to send in this call
     * @param {api-callback} callback - The callback for this call
     */
    doGet(partialUrl, params, callback) {
        let contentType = "application/json";
        let accept = "application/json";
        if (this.isEncrypted) {
            contentType = "application/jose+json";
            accept = "application/jose+json";
            ApiClient.createJoseJsonParser();
        }
        request
            .get(`${this.server}/rest/v4/${partialUrl}`)
            .auth(this.username, this.password)
            .set("User-Agent", `Hyperwallet Node SDK v${this.version}`)
            .set("x-sdk-version", this.version)
            .set("x-sdk-type", "NodeJS")
            .set("x-sdk-contextId", this.contextId)
            .type(contentType)
            .accept(accept)
            .query(params)
            .end(this.wrapCallback("GET", callback));
    }

    /**
     * Wrap a callback to process possible API and network errors
     *
     * @param {string} httpMethod - The http method that is currently processing
     * @param {api-callback} callback - The final callback
     * @returns {function(err: Object, res: Object)} - The super agent callback
     *
     * @private
     */
    wrapCallback(httpMethod, callback = () => null) {
        return (err, res) => {
            const contentTypeHeader = res && res.header ? res.header["content-type"] : undefined;
            if (!err) {
                const expectedContentType = (this.isEncrypted) ? "application/jose+json" : "application/json";
                if (res && res.status !== 204 && contentTypeHeader && !contentTypeHeader.includes(expectedContentType)) {
                    callback([{
                        message: "Invalid Content-Type specified in Response Header",
                    }], res ? res.body : undefined, res);
                    return;
                }
            }
            if (this.isEncrypted && contentTypeHeader && contentTypeHeader.includes("application/jose+json")
              && res.body && !ApiClient.isEmptyResponseBody(res.body)) {
                this.processEncryptedResponse(httpMethod, err, res.body, callback);
            } else {
                this.processNonEncryptedResponse(err, res, callback);
            }
        };
    }

    /**
     * Process non encrypted response from server
     *
     * @param {Object} err - Error object
     * @param {Object} res - Response object
     * @param {api-callback} callback - The final callback
     *
     * @private
     */
    processNonEncryptedResponse(err, res, callback) {
        if (!err) {
            const formattedRes = ApiClient.formatResForCallback(res);
            callback(undefined, formattedRes.body, formattedRes);
            return;
        }

        let errors = [
            {
                message: err.status ? err.message : `Could not communicate with ${this.server}`,
                code: err.status ? err.status.toString() : "COMMUNICATION_ERROR",
            },
        ];
        if (res && res.body && res.body.errors) {
            // eslint-disable-next-line prefer-destructuring
            errors = res.body.errors;
        }
        callback(errors, res ? res.body : undefined, res);
    }

    /**
     * Process encrypted response from server
     *
     * @param {string} httpMethod - The http method that is currently processing
     * @param {Object} err - Error object
     * @param {Object} res - Response object
     * @param {api-callback} callback - The final callback
     *
     * @private
     */
    processEncryptedResponse(httpMethod, err, res, callback) {
        this.encryption.decrypt(res)
            .then((decryptedData) => {
                const responseBody = JSON.parse(decryptedData.payload.toString());
                if (responseBody.errors) {
                    const responseWithErrors = {};
                    responseWithErrors.body = responseBody;
                    this.processNonEncryptedResponse(responseBody, responseWithErrors, callback);
                } else {
                    const formattedRes = ApiClient.formatResForCallback({ body: responseBody });
                    callback(undefined, formattedRes.body, decryptedData);
                }
            })
            .catch(() => callback([{ message: `Failed to decrypt response for ${httpMethod} request` }], res, res));
    }

    /**
     * Creates response body parser for application/jose+json content-type
     *
     * @private
     */
    static createJoseJsonParser() {
        request.parse["application/jose+json"] = (res, callback) => {
            let data = "";
            res.on("data", (chunk) => {
                data += chunk;
            });
            res.on("end", () => {
                callback(null, data);
            });
        };
    }

    /**
     * Helper function to check if the response body is an empty object
     *
     * @private
     */
    static isEmptyResponseBody(body) {
        return Object.keys(body).length === 0 && Object.getPrototypeOf(body) === Object.prototype;
    }
}