middleware/component.js

'use strict';

const { MiddlewareAbstract } = require("./abstract");
const { ComponentRegistry } = require("../lib/component/registry");
const { ComponentShell: Shell } = require("../lib/component/shell");
const { STATUS_CODE } = require('./codes');

/**
 * Options for configuring bots custom component middleware.
 * @typedef {Object} ComponentMiddlewareOptions
 * @memberof module:Middleware
 * @property {string} [baseUrl='/'] - Base url for custom component endpoints
 * @property {string} [cwd=process.cwd()] - Working directory from which any component paths are relative.
 * @property {(string[]|Object[]|Function[])} register - Series of paths to components or directories, Objects with name=>component pairs, Objects representing a component, or Component class ctor Functions.
 * @property {*} [mixins] - Any mixin properties for CustomComponentContext
 */

/**
 * define req.param keys
 */
const [PARAM_COMPONENT] = ['component'];

/**
 * ComponentMiddleware.
 * @extends MiddlewareAbstract
 * @memberof module:Middleware
 * @private
 */
class ComponentMiddleware extends MiddlewareAbstract {

  _init(service, options) {
    if (!service || typeof service.get !== 'function' || typeof service.post !== 'function') {
      throw new Error('Cannot initialize component middleware: service argument is required');
    }
    const opts = Object.assign({
      // option defaults
      baseUrl: '/',
      register: [],
      mixins: {}
    }, options);

    /**
     * assemble root registry from provided `register` property
     * merge explicitly provided component registry with the hierarchical fs registry.
     */
    const rootRegistry = ComponentRegistry.create(opts.register, opts.cwd);
    
    const { baseUrl } = opts;
    /**
     * establish component metadata index
     */
    service.get(this.__endpoint(baseUrl, '/'), (req, res) => {
      const meta = this.__getShell(rootRegistry)
        .getAllComponentMetadata();
      res.json(meta);
    });

    /**
     * handle custom component invocation
     */
    service.post(this.__endpoint(baseUrl, `/:${PARAM_COMPONENT}`), (req, res) => {
      const componentName = req.params[PARAM_COMPONENT];
      // invoke
      this.__invoke(componentName, rootRegistry, opts, req, res);
    });

    /**
     * handle ResolveEntities event handler invocation
     */
    service.post(this.__endpoint(baseUrl, `/resolveentities/:${PARAM_COMPONENT}`), (req, res) => {
      const componentName = req.params[PARAM_COMPONENT];
      const mixins = Object.assign({}, opts.mixins);
      this.__getShell(rootRegistry).invokeResolveEntitiesEventHandler(componentName, req.body, this.__invocationCb(res), mixins);      
    });

    /**
     * handle DataQuery event handler invocation
     */
    service.post(this.__endpoint(baseUrl, `/dataquery/:${PARAM_COMPONENT}`), (req, res) => {
      const componentName = req.params[PARAM_COMPONENT];
      const mixins = Object.assign({}, opts.mixins);
      this.__getShell(rootRegistry).invokeDataQueryEventHandler(componentName, req.body, this.__invocationCb(res), mixins);      
    });

    /**
     * handle LLM transformation handler invocation
     */
    service.post(this.__endpoint(baseUrl, `/llmtransformation/:${PARAM_COMPONENT}`), (req, res) => {
      const componentName = req.params[PARAM_COMPONENT];
      const mixins = Object.assign({}, opts.mixins);
      this.__getShell(rootRegistry).invokeLlmTransformationHandler(componentName, req.body, this.__invocationCb(res), mixins);      
    });

    /**
     * handle LLM component handler invocation
     */
    service.post(this.__endpoint(baseUrl, `/llmcomponent/:${PARAM_COMPONENT}`), (req, res) => {
      const componentName = req.params[PARAM_COMPONENT];
      const mixins = Object.assign({}, opts.mixins);
      this.__getShell(rootRegistry).invokeLlmComponentHandler(componentName, req.body, this.__invocationCb(res), mixins);      
    });

  }

  /**
   * construct an endpoint from base and url
   * @param {string} base - base url
   * @param {string} url - endpoint url
   */
  __endpoint(base, url) {
    return '/' + [base, url].map(part => part.replace(/^\/|\/$/g, ''))
      .filter(part => !!part)
      .join('/');
  }

  /**
   * get Shell methods
   * @param registry - The registry for the invocation shell
   * @private
   */
  __getShell(registry) {
    return Shell({
      logger: this._logger
    }, registry);
  }

  /**
   * invoke the component shell.
   * @param componentName: string - component name
   * @param registry - registry to which the component belongs
   * @param options - Middleware options reference.
   * @param req - MobileCloudRequest
   * @param res - express.Response
   * @private
   */
  __invoke(componentName, registry, options, req, res) {
    // apply mixins and invoke component
    const mixins = Object.assign({}, options.mixins);
    if (req.oracleMobile) {
      mixins.oracleMobile = req.oracleMobile;
    }
    this.__getShell(registry)
      .invokeComponentByName(componentName, req.body, mixins, this.__invocationCb(res));
  }
  /**
   * convenience handler for CC invocation
   * @param res: express.Response
   * @private
   */
  __invocationCb(res) {
    return (err, data) => {
      if (!err) {
        res.status(STATUS_CODE.OK).json(data);
      } else {
        switch (err.name) {
        case 'unknownComponent':
          res.status(STATUS_CODE.NOT_FOUND).send(err.message);
          break;
        case 'badRequest':
          res.status(STATUS_CODE.BAD_REQUEST).send(err.message);
          break;
        default:
          res.status(STATUS_CODE.INTERNAL_SERVER_ERROR).send(err.message);
          break;
        }
      }
    };
  }
}

module.exports = {
  ComponentMiddleware,
};