adam-mccormick/moleculer-axios

View on GitHub
src/index.js

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * moleculer-request
 * Copyright (c) 2019 Adam McCormick (https://github.com/amccormick/moleculer-request)
 * MIT Licensed
 */
"use strict";

const _     = require("lodash");
const axios = require("axios");
const qs    = require("qs");

const MoleculerError = require("moleculer").Errors.MoleculerServerError;

const METHODS = ["get", "put", "post", "delete", "patch", "options", "head", "request"];

class MoleculerAxiosError extends MoleculerError {

}

const RESPONDERS = {
    full: (res) => res,
    data: (res) => res.data,
    headers: (res) => res.headers,
    status: (res) => res.status,
    ok: (res) => res.status < 400,
};

const SAFE_METHOD_PARAMS = {
    url: {
        type: "string",
        optional: true
    },
    query: [
        {
            type: "string",
            optional: true
        },
        {
            type: "object",
            optional: true
        }
    ],
    config: {
        type: "object",
        optional: true
    }
};

const UNSAFE_METHOD_PARAMS = {
    url: {
        type: "string",
        optional: true
    },
    data: {
        type: "any",
        optional: true
    },
    config: {
        type: "object",
        optional: true
    }
};

module.exports = {

    name: "axios",

    settings: {

        /**
         * Axios configuration settings.
         */
        axios: {

            /**
             * An array of axios methods to expose as actions of this
             * service. Any methods NOT listed here will be removed
             * from the resulting service instance.
             *
             * By default all axios request methods are exposed
             *
             * Any empty array will not expose any actions which is useful
             * if you are using this as a mixin and want to create endpoint
             * actions which delegate to the underlying axios instance.
             */
            expose: null,

            /**
             * Defines what part of the response should be sent as the result
             * of the action call.
             *
             * Possible values are:
             * - `full` return the entire response object
             * - `data` return only the response data
             * - `headers` return only the response headers
             * - `status` return only the status code
             * - `ok` return a boolean indicating if the request was successful (i.e < 400)
             *
             * You can also supply a function which takes the full response
             * to return any custom result object
             */
            responder: "full",

            /**
             * Configure the underlying axios instance with a
             * standard axios config object
             *
             * https://github.com/axios/axios#config-defaults
             */
            config: {
                paramsSerializer: function (params) {
                    return qs.stringify(params, { arrayFormat: "brackets" });
                }
            },

            /**
             * Configure how request and responses should be
             * logged.
             */
            logging: {
                level: "info",
                // request: {
                //     include: ["url", "method"]
                // },
                // response: {
                //     include: ["config.url", "status", "statusText"]
                // }
            }
        },
    },

    /**
     * Actions
     */
    actions: {
        request: {
            params: {
                config: "object"
            },

            handler(ctx) {
                return this.send(undefined, ctx);
            }
        },

        get: {
            params: SAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("get", ctx);
            }
        },

        put: {
            params: UNSAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("put", ctx);
            }
        },

        post: {
            params: UNSAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("post", ctx);
            }
        },

        delete: {
            params: UNSAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("delete", ctx);
            }
        },

        patch: {
            params: UNSAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("patch", ctx);
            }
        },

        options: {
            params: SAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("options", ctx);
            }
        },

        head: {
            params: SAFE_METHOD_PARAMS,

            handler(ctx) {
                return this.send("head", ctx);
            }
        }
    },

    /**
     * Methods
     */
    methods: {

        send(method, ctx) {

            const config = ctx.params.config || {};

            // use the action method if specified even if one was set on the config
            if(method)
                config.method = method;

            _.defaults(config, {
                url: ctx.params.url,
                params: ctx.params.query,
                ctx
            });

            return this.axios.request(config)
                .then(response => this.$responder(response));
        }
    },

    /**
     * Service created lifecycle event handler which constructs
     * and configures axios instance for this service and removes
     * actions which are declared to not be exposed.
     *
     */
    created() {
        const { config, responder, expose, logging } = this.settings.axios;

        this.axios = axios.create(config);

        this.$responder = _.isFunction(responder) ? this.settings.axios.responder : RESPONDERS[responder];

        if(expose && _.isArray(expose)){
            const exclude = _.difference(METHODS, expose.map((v) => v.toLowerCase()));
            exclude.forEach(action => {
                this.actions[action] = false;
            });
        }

        // TODO: crap logging
        if(logging && logging.level in this.logger) {
            this.axios.interceptors.request.use((config) => {
                // TODO get uri method of axios is broken
                this.logger[logging.level](`=> ${config.method.toUpperCase()} ${this.axios.getUri(config)}`);
                return config;
            });

            this.axios.interceptors.response.use((response) => {
                this.logger[logging.level](`<= ${response.status} - ${response.statusText} ${this.axios.getUri(response.config)}`);
                return response;
            });

            this.axios.interceptors.response.use(null, err => {
                if (err.response) {
                    // The request was made and the server responded with a status code
                    // that falls out of the range of 2xx
                    this.logger.error(`Received error response: ${err.response.status} - ${err.response.statusText}`, err.response.data);
                }
                else if (err.request) {
                    // The request was made but no response was received
                    this.logger.error("No response received", err.message);
                }
                else {
                    // Something happened in setting up the request that triggered an Error
                    this.logger.error("Error creating request", err.message);
                }
                return this.Promise.reject(new MoleculerAxiosError(err.message, 500, "HTTP_REQUEST_ERROR"));
            });
        }
    },

    METHODS
};