lucid-services/serviser

View on GitHub
lib/remoteServiceManager.js

Summary

Maintainability
A
2 hrs
Test Coverage
A
96%
const path = require('path');
const _ = require('lodash');

module.exports = RemoteServiceManager;
module.exports.RemoteServiceManager = RemoteServiceManager;


/**
 * A factory for `ServiceSDK` instances
 * (see affiliated [serviser-sdk](https://github.com/lucid-services/serviser-sdk) plugin)
 * and {@tutorial 4.SDK-integration}
 *
 * @param {Object} services
 * @param {Object} services.<serviceName> - index name = name of the service
 * @param {Object} services.<serviceName>.<scope> - index name not constrained, could be anything... eg.: specific service application name (in case multiple applications are listening under one service) or access level type eg.: "public" or "s2s"
 * @param {String} services.<serviceName>.<scope>.npm - npm sdk package name which exports a literal object of versioned ServiceSDKs. (use either `dir` or `npm option)
 * @param {String} services.<serviceName>.<scope>.dir - sdk package directory path which exports object that inherits ServiceSDK. (use either `dir` or `npm option)
 * @param {String} services.<serviceName>.<scope>.host
 * @param {Boolean} services.<serviceName>.<scope>.protocol
 *
 * @constructor
 */
function RemoteServiceManager(services) {
    this.options = services || {};
    this.services = {};

    /**
     * `ServiceSDK` constructor
     * @name RemoteServiceManager#ServiceSDK
     * @instance
     * @type {Function}
     */
    let sdk = require('serviser-sdk');
    this.ServiceSDK = sdk.ServiceSDKInterface;
}


/**
 * registers `ServiceSDK` instance
 *
 * @public
 * @param {String} key - format `<serviceName>:<scope>`
 * @param {ServiceSDK} sdk
 *
 * @return {RemoteServiceManager} - self
 */
RemoteServiceManager.prototype.add = function(key, sdk) {
    let keySegments = key.split(':');

    if (!(sdk instanceof this.ServiceSDK)) {
        throw new Error('sdk must be instanceof ServiceSDK');
    }

    if (keySegments.length < 2) {
        throw new Error('The first argument must be in format: `<serviceName>:<scope>`');
    }

    _.set(this.services, keySegments.concat([sdk.version]), sdk);

    return this;
};


/**
 * @public
 * @param {String} key - format `<serviceName>:<scope>:<version>`
 *
 * @return {ServiceSDK}
 */
RemoteServiceManager.prototype.get = function(key) {
    let remoteService = _.get(this.services, key.split(':'));

    if (!(remoteService instanceof this.ServiceSDK)) {
        throw new Error(`${key} remote service not found`);
    }

    return remoteService;
};


/**
 * @public
 * @param {String} key - format `<serviceName>:<scope>:<version>` (scope|version key segments are optional)
 *
 * @return {Boolean}
 */
RemoteServiceManager.prototype.has = function(key) {
    let segments = key.split(':');

    if (segments.length > 3) {
        throw new Error(`${key} should be in format: <serviceName>:<scope>:<version>`);
    }

    return _.has(this.services, key.split(':'));
};


/**
 * constructs specific `ServiceSDK` from configuration provided to the constructor  
 * the specific `ServiceSDK` is supposed to be auto generated by `serviser-sdk` plugin
 * and published as a npm package
 *
 * @example
 * //config.js:
 * module.exports = {
 *    services: {
 *       user: {
 *         public: {
 *           protocol: 'https',
 *           host: "127.0.0.1:4001",
 *           npm: "service-name-app-name-sdk"
 *         }
 *       }
 *    }
 * }
 *
 * or
 *
 * module.exports = {
 *    services: {
 *       user: {
 *         public: {
 *           protocol: 'https',
 *           host: "127.0.0.1:4001",
 *           dir: "/absolute/path/to/my/sdk/directory"
 *         }
 *       }
 *    }
 * }
 *
 * //method usage
 * remoteServiceManager.buildRemoteService('user:public:v1.0');
 *
 * @public
 *
 * @param {String} key - format: `<serviceName>:<scope>:<version>`
 * @param {Object} [options] - `ServiceSDK` constructor options
 *
 * @throws {Error}
 * @return {ServiceSDK}
 */
RemoteServiceManager.prototype.buildRemoteService = function(key, options) {
    let keySegments = key.split(':');
    let serviceName = keySegments[0];
    let scope       = keySegments[1];
    let sdkPath     = keySegments.slice(2);
    let version     = sdkPath.pop();

    if (keySegments.length < 3) {
        throw new Error('The first argument must be in format: `<serviceName>:<scope>:<version>`');
    }

    let confPath = `${serviceName}:${scope}`;
    let conf = _.get(this.options, [serviceName, scope]);

    if (!conf) {
        throw new Error(`Cant find config value of "${confPath}"`);
    }

    if (!_.isPlainObject(conf) || (!conf.npm && !conf.dir)) {
        throw new Error(`buildRemoteService requires "services:${confPath}:npm"
        or "services:${confPath}:dir" config option to be set.
        Cant connect remote service.`);
    }

    if (conf.dir && !path.isAbsolute(conf.dir)) {
        throw new Error(`"dir" option value is not an absolute path.
        Relative paths not supported`);
    }

    let sdkVersions = module.require(conf.npm || conf.dir);
    let sdkContructorPath = sdkPath.concat([version]);
    let sdkContructor = _.get(sdkVersions, sdkContructorPath);

    if (!sdkContructor) {
        throw new Error(`${confPath} service sdk does not have version: ${sdkContructorPath}`);
    }

    let opt =_.omit(conf, 'protocol', 'host', 'npm');
    Object.assign(opt, options || {});

    let protocol = conf.protocol;
    if (conf.host) {
        opt.baseURL =  protocol + '://' + conf.host;
    }

    let sdk = new sdkContructor(opt);

    this.add(
        `${serviceName}:${scope}${sdkPath.length ? ':' + sdkPath.join(':') : ''}`,
        sdk
    );

    return sdk;
};