Manual Reference Source Test

src/helpers/Query.js

import ContentService from '../services/ContentService';
import Content from './content/Content';
import { validate, isValid } from '../utilities/ValidationUtility';

/**
 * Helper class representing a Query object that can be passed on to the findDynamicContent method
 */
class Query {
  /**
   * Constructs the Query class
   *
   * @param {!(String|Content)} resource    - The type of content it is.
   * @param {Array}   [orderBy=[]]          - an array objects that define the logic to order the
   *                                          results by
   * @param {Number}  [page=0]              - the page number of the results given
   * @param {Number}  [perPage=0]           - the number of content to return per page
   * @param {Boolean} [minimal=false]       - returns only the filtered attributes specified
   * @param {Boolean} [brandRelative=false] - returns only the contents that belong to brands the
   *                                          mobile_user/device favorited
   *
   * @example
   * const query = new Query('acronyms')
   *  .ascending('name')
   *  .setResource('type')
   *  .addSearchValue('blue')
   *  .addSearchValue('green')
   *  .addSearchValue('red')
   *  .clearSearchValues()
   *  .searchByAddressMode()
   *  .setSwLat('10.0')
   *  .setSwLong('9.9')
   *  .setNeLat('9.9')
   *  .limit(10)
   *  .showMinimal()
   *  .showBrandRelative()
   *  .referenceModule('reference')
   *  .find();
   */
  constructor(resource, orderBy = [], page = 0, perPage = 0, minimal = false,
    brandRelative = false) {
    /** @type {String} */
    this._id = undefined;
    /** @type {String|Content} */
    this._resource = resource;
    /** @type {Object[]} */
    this._filters = [];
    if (!Array.isArray(orderBy)) {
      throw new Error('Query construction error: orderBy is not an array');
    }

    /** @type {Array} */
    this.orderBy = orderBy;
    /** @type {Number} */
    this.page = page;
    /** @type {Number} */
    this.perPage = perPage;
    /** @type {Boolean} */
    this.minimal = minimal;
    /** @type {Boolean} */
    this.brandRelative = brandRelative;

    /** @type {Boolean} */
    this.whole_word = false;
    /** @type {Boolean} */
    this.case_sensitive = false;

    /** @type {String} */
    this.reference_module = '';

    // Set default search mode to 'search'
    this.newFilter();
  }

  /** @type {Object} */
  get filter() {
    return this._filters[this._filters.length - 1];
  }

  /** @type {Array<Object>} */
  get filters() {
    return this._filters;
  }

  /**
   * @param {!(String|Object)} resource - Must be a string or have the attribute 'resource'
   * @type {String}
   */
  set resource(resource) {
    // check param values are correct
    if (!(resource instanceof Content) && typeof resource !== 'string'
      && !(resource instanceof String)) {
      throw new Error('Query setResource err: resource must either be a String or a Content');
    }

    // If of type Content, grab the resource attribute
    if (resource instanceof Content) {
      this._resource = resource.resource;
    } else {
      if (resource.length < 1) {
        throw new Error('Query setResource err: resource must be a valid String');
      }
      this._resource = resource;
    }
  }

  /** @type {String} */
  get resource() {
    return this._resource;
  }

  /**
   * Function to pass in a Dynamic Content to search. Takes a Dynamic Content
   * response and checks for the 'content_type'.
   *
   * @param {Object} resource - Either an object of type Content or a String permanent link value.
   * @return {Query} this
   *
   * @example
   * const q = new Query('labs');
   * q.setResource('acronyms');
   */
  setResource(resource) {
    this.resource = resource;
    return this;
  }

  /**
   * Validates all possible types of all filters in Query for a filter call.
   *
   * @throws {Clinical6Error} - If missing required parameters or Query fields
   * @return {Boolean}        - True if this is a valid Query
   *
   * @example
   * const q = new Query('acronyms');
   * q.validate();
   */
  validate() {
    this._filters.forEach((filter) => {
      const allBox = filter.sw_lat && filter.sw_lng && filter.ne_lat && filter.ne_lng;
      const anyBox = filter.sw_lat || filter.sw_lng || filter.ne_lat || filter.ne_lng;
      const allRadius = filter.radius && filter.exact_lat && filter.exact_lng;
      const anyRadius = filter.radius || filter.exact_lat || filter.exact_lng;

      switch (filter.attribute) {
        case 'address':
          break;
        case 'date':
          validate('Query.validate mode date',
            isValid(!(!filter.min && !filter.max), 'filter min or max values not specified'));
          break;
        case 'location':
          if ((!filter.values || (filter.values && filter.values.length < 1))
            && !anyBox && !anyRadius) {
            throw new Error('location address value not specified');
          } else if (anyRadius && !allRadius) {
            throw new Error('location radius exact lng and exact lat not specified');
          } else if (anyBox && !allBox) {
            throw new Error('location not all box coordinates specified');
          }
          break;
        case 'numeric':
          validate('Query.validate mode numeric',
            isValid(!(!filter.value && !filter.values
              && !filter.min && !filter.max),
            'numeric min, max, or values not specified'));
          break;
        case 'search':
          validate('Query.validate mode search',
            isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
          break;
        case 'tag':
          validate('Query.validate mode tag',
            isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
          break;
        case 'tag_names':
          validate('Query.validate mode tag_names',
            isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
          break;
        default: break;
      }
    });
  }

  /**
   * Creates a new filter with the attribute
   *
   * @param {String} attribute  - The new attribute of the filter
   * @return {Query}            - this
   *
   * @example
   * const q = new Query('acronyms');
   * q.searchByLocationMode();  // updates existing current filter
   * q.newFilter('tag'); // creates entirel new filter entry with the attribute 'tag'
   */
  newFilter(attribute = 'search') {
    /** @type {Object} */
    this.filters.push({
      attribute
    });
    return this;
  }

  /**
   * Executes the search. Prior to calling this method, search values must be added.
   * Additionally, a search method should be set. By default, the search mode will be 'search'.
   *
   * @return {Promise<Content[]>} - A promise with a list of content.
   *
   * @example
   * const q = new Query('acronyms');
   * q.find();
   */
  find() {
    return (new ContentService()).find(this);
  }

  /** Query Search Values */

  /**
   * Adds a search value to search for in the Dynamic Content.
   *
   * @param {String | Number} searchValue - the string or number value to add.
   * @return {Query} this
   */
  addSearchValue(searchValue) {
    if (!this.filter.values) {
      this.filter.values = [];
    }
    this.filter.values.push(searchValue);
    return this;
  }

  /**
   * Removes the provided search value from the array of search values. Throws
   * an error if the provided value is not in the search array.
   *
   * @param {String} searchValue - the string value to remove.
   * @return {Query} this
   */
  removeSearchValue(searchValue) {
    if (this.filter.values) {
      const index = this.filter.values.indexOf(searchValue);
      if (index < 0) {
        throw new Error('Query removeSearchValue err: searchValue not found');
      }
      this.filter.values.splice(index, 1);
    }
    return this;
  }

  /**
   * Clears the array of search values.
   *
   * @return {Query} this
   */
  clearSearchValues() {
    this.filter.values = [];
    return this;
  }

  /**
   * Sets the search mode to 'search'. Searches a Dyanmic Content to find
   * the values in filter.values.
   *
   * @return {Query} this
   */
  searchMode() {
    this.filter.attribute = 'search';
    return this;
  }

  /**
   * Sets the search mode to 'starts_with'. Searches a Dyanmic Content to find
   * matches that start with the values provided in filter.values.
   *
   * @return {Query} this
   */
  startsWithMode() {
    this.filter.search_method = 'starts_with';
    return this;
  }

  /**
   * Sets the search mode to 'ends_with'. Searches a Dyanmic Content to find
   * matches that end with the values provided in searchValues.
   *
   * @return {Query} this
   */
  endsWithMode() {
    this.filter.search_method = 'ends_with';
    return this;
  }

  /**
   * Sets the search mode to 'whole_word'. Searches a Dyanmic Content to find
   * matches that end with the values provided in searchValues.
   *
   * @return {Query} this
   */
  wholeWordMode() {
    this.filter.search_method = 'whole_word';
    return this;
  }

  /**
   * Creates a filter that would force the search to check for whole words
   *
   * @return {Query} this
   */
  wholeWordSearch() {
    /** @type {Boolean} */
    this.filter.whole_word = true;
    return this;
  }

  /**
   * Creates a filter that would force the search to be case sensitive
   *
   * @return {Query} this
   */
  makeCaseSensitive() {
    /** @type {Boolean} */
    this.filter.case_sensitive = true;
    return this;
  }

  /**
   * Sets the max of filter
   *
   * @param  {Number|String} num - number to set max value to
   * @return {Query} this
   */
  lessThanOrEqualTo(num) {
    /** @type {Number|String} */
    this.filter.max = num;
    return this;
  }

  /**
   * Sets the min of the filter
   *
   * @param  {Number} num - number to set min value to
   * @return {Query} this
   */
  greaterThanOrEqualTo(num) {
    /** @type {Number|String} */
    this.filter.min = num;
    return this;
  }

  /**
   * Sets the exact number to filter by for search by number.
   *
   * @param  {Number} num - number to filter by
   * @return {Query} this
   */
  setValue(num) {
    /** @type {Number|String} */
    this.filter.value = num;
    return this;
  }

  /**
   * Builds a query order ascending order based on the column givenf
   *
   * @param  {String} col - the column/attribute to sort by
   * @return {Query} this
   */
  ascending(col) {
    const orderBy = {};
    orderBy.column = col;
    orderBy.direction = 'asc';
    this.orderBy.push(orderBy);
    return this;
  }

  /**
   * Builds a query order descending order based on the column given
   *
   * @param  {String} col - the column/attribute to sort by
   * @return {Query} this
   */
  descending(col) {
    const orderBy = {};
    orderBy.column = col;
    orderBy.direction = 'desc';
    this.orderBy.push(orderBy);
    return this;
  }

  /**
   * Builds a query with limit of how many results to show
   *
   * @param  {Number} num - number of results to show
   * @return {Query} this
   */
  limit(num) {
    if (Number.isNaN(num)) {
      throw new Error('Query limit error: num is not a number');
    }
    this.page = 1;
    this.perPage = num;
    return this;
  }

  /**
   * Builds a query that returns only the minimal attributes specified in the filters
   *
   * @return {Query} this
   */
  showMinimal() {
    this.minimal = true;
    return this;
  }

  /**
   * Builds the query that returns only the contents that belong to brands the
   *  mobile_user/device favorited
   *
   * @return {Query} this
   */
  showBrandRelative() {
    this.brandRelative = true;
    return this;
  }

  /**
   * Builds the query to specify the module that will be used to look for favorited contents
   *
   * @param  {String} val - the string value of the module
   * @return {Query} this
   */
  referenceModule(val) {
    this.reference_module = val;
    return this;
  }

  /**
   * Sets the search method to 'address'
   *
   * @return {Query} this
   */
  searchByAddressMode() {
    this.filter.attribute = 'address';
    return this;
  }

  /**
   * Sets the search method to 'date'.
   *
   * @return {Query} this
   */
  searchByDateMode() {
    this.filter.attribute = 'date';
    return this;
  }

  /**
   * Set the search method to 'location'
   *
   * @return {Query} this
   */
  searchByLocationMode() {
    this.filter.attribute = 'location';
    return this;
  }

  /**
   * Sets the search method to 'numeric'.
   *
   * @return {Query} this
   */
  searchByNumberMode() {
    this.filter.attribute = 'numeric';
    return this;
  }

  /**
   * Sets the search method to 'tag'.
   *
   * @return {Query} this
   */
  searchByTagIDMode() {
    this.filter.attribute = 'tag';
    return this;
  }

  /**
   * Sets the search method to 'tag_names'.
   *
   * @return {Query} this
   */
  searchByTagNamesMode() {
    this.filter.attribute = 'tag_names';
    return this;
  }

  /**
   * Sets the radius for location search.
   *
   * @param  {Number|String} radius - number to set the radius value to
   * @return {Query} this
   */
  setRadius(radius) {
    /** @type {Number|String} */
    this.filter.radius = radius;
    return this;
  }

  /**
   * Sets the exact latitude value for location search/
   *
   * @param  {Number|String} latitude - number to set exact latitude value to
   * @return {Query} this
   */
  setExactLatitude(latitude) {
    /** @type {Number|String} */
    this.filter.exact_lat = latitude;
    return this;
  }

  /**
   * Sets the exact longitude value for location search.
   *
   * @param  {Number|String} longitude - number to set exact longitude value to
   * @return {Query} this
   */
  setExactLongitude(longitude) {
    /** @type {Number|String} */
    this.filter.exact_lng = longitude;
    return this;
  }

  /**
   * Sets the SW latitude value for location search
   *
   * @param  {Number|String} latitude - number to set SW latitude value to
   * @return {Query} this
   */
  setSwLat(latitude) {
    /** @type {Number|String} */
    this.filter.sw_lat = latitude;
    return this;
  }

  /**
   * Sets the SW longitude value for location search.
   *
   * @param  {Number|String} longitude - number to set SW longitude value to
   * @return {Query} this
   */
  setSwLong(longitude) {
    /** @type {Number|String} */
    this.filter.sw_lng = longitude;
    return this;
  }

  /**
   * Sets the NE latitude value for location search.
   *
   * @param  {Number|String} latitude - number to set NE latitude value to
   * @return {Query} this
   */
  setNeLat(latitude) {
    /** @type {Number|String} */
    this.filter.ne_lat = latitude;
    return this;
  }

  /**
   * Sets the NE longitude value for location search.
   *
   * @param  {Number|String} longitude - number to set NE longitude value to
   * @return {Query} this
   */
  setNeLong(longitude) {
    /** @type {Number|String} */
    this.filter.ne_lng = longitude;
    return this;
  }

  /**
   * Sets the NW longitude value for location search.
   *
   * @param  {Number|String} longitude - number to set NW longitude value to
   * @return {Query} this
   */
  setNwLong(longitude) {
    /** @type {Number|String} */
    this.filter.nw_lng = longitude;
    return this;
  }
}

export default Query;