src/executor.js

Summary

Maintainability
A
1 hr
Test Coverage
"use strict";

const compose = require("koa-compose");
const path = require("path");
const {exists} = require("./utils");
const Adapter = require("./adapter");
const Criteria = require("./criteria");

/**
 * Main class which is responsible for proceeding of criteria execution.
 */
class Executor {

  /**
   * @param {string|string[]} path Array of folders where custom filters are located. String can be passed.
   */
  constructor({path = []}) {
    if (typeof path === "string") {
      path = [path];
    }
    this.opts = {
      path
    };
  }

  /**
   * @param {Adapter|{}} adapter
   * @throws
   */
  setAdapter(adapter) {
    if (adapter instanceof Adapter || ["getProvider", "execute"].every(fn => typeof adapter[fn] === "function")) {
      this.adapter = adapter;
      return;
    }
    throw new Error("You must pass valid adapter object.");
  }

  /**
   * Find filter's folder path
   *
   * @param {string} filter
   * @returns {string}
   * @throws
   */
  resolveFilterPath(filter) {
    let filterPath = this.opts.path.find(p => exists(path.join(p, `${filter}.js`)));
    if (typeof filterPath === "undefined") {
      throw new Error(`"${filter}" filter not found`);
    }
    return path.join(filterPath, filter);
  }

  /**
   * Normalize filters map passed from criteria. This method finds each implementation of passed filter and initializes
   * it. If filter was passed as function, that's mean that it is implementation itself.
   *
   * @param {{}} filtersMap
   * @returns {function[]}
   */
  resolveFilters(filtersMap) {
    const results = Object.keys(filtersMap)
      .map(name => {
        if (typeof name === "function") {
          return name;
        }
        const filterPath = this.resolveFilterPath(name);
        const filterModule = require(filterPath);
        const filterOpts = filtersMap[name];

        return Array.isArray(filterOpts) ? filterOpts.map(opts => filterModule(opts)) : filterModule(filtersMap[name]);
      });
    return [].concat.apply([], results);
  }

  /**
   * Default very lightweight mapper
   *
   * @param {{}} obj
   * @param {{}} rules
   * @returns {{}}
   */
  mapValues(obj, rules = {}) {
    return Object.keys(obj)
      .reduce((result, key) => {
        if (rules.hasOwnProperty(key)) {
          if (rules[key] === false) {
            // if column is marked as `false` skip it
            return result;
          }
          if (typeof rules[key] === "function") {
            result[key] = rules[key](obj[key]);
            return result;
          }
          result[rules[key]] = obj[key];
        } else {
          // if rules doesn't contain property, use it with the same name
          result[key] = obj[key];
        }

        return result;
      }, {})
  }

  /**
   * Helper function to return first element of result
   *
   * @param {Criteria|{}} criteria
   */
  findOne(criteria) {
    return this.execute(criteria)
      .then(r => {
        if (Array.isArray(r)) {
          return r[0];
        }
        return r;
      });
  }

  /**
   * Build query by passed criteria and run it. Each filter can be defined as async await function or return Promise.
   *
   * @param {Criteria|{}} criteria
   */
  execute(criteria) {
    if (criteria instanceof Criteria || ["filters", "source", "mappers"].every(fn => fn in criteria)) {
      const filtersFn = this.resolveFilters(criteria.filters);
      const fn = compose(filtersFn);

      let provider = this.adapter.getProvider(criteria.source);

      return fn(provider)
        .then(() => {
          return this.adapter.execute(provider);
        })
        .then((results) => {
          if (!criteria.mappers.length) {
            return results;
          }
          if (!Array.isArray(results)) {
            results = [results];
          }
          return criteria.mappers.reduce((results, mapper) => {
            if (typeof mapper === "function") {
              // use custom mapper function that is provided with criteria
              return results.map(r => mapper(r));
            } else if (typeof mapper === "object" && !Array.isArray(mapper)) {
              // if mapper was passed as plain object try to map results like column -> field
              return results.map(r => this.mapValues(r, mapper))
            }
            return results;
          }, results);
        })
        .catch((err) => {
          console.error(err);
          throw err;
        });
    }
    throw new Error("You must pass valid criteria object.");
  }
}

module.exports = Executor;