Source: listquery.js

/*
 * (c) Miva Inc <https://www.miva.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * $Id: listquery.js 72815 2019-01-23 00:00:04Z gidriss $
 */

const { Request, Response } = require('./abstract');
const util = require('./util');

/** @module ListQuery */

/** Sort Constants */
/** @ignore */
const SORT_ASCENDING    = 'asc';
/** @ignore */
const SORT_DESCENDING   = 'desc';

/** Operator Constants */
/** @ignore */
const OPERATOR_EQ       = 'EQ';         /** Operator: Equals */
/** @ignore */
const OPERATOR_GT       = 'GT';         /** Operator: Greater Than */
/** @ignore */
const OPERATOR_GE       = 'GE';         /** Operator: Greater Than or Equal */
/** @ignore */
const OPERATOR_LT       = 'LT';         /** Operator: Less Than */
/** @ignore */
const OPERATOR_LE       = 'LE';         /** Operator: Less Than or Equal */
/** @ignore */
const OPERATOR_CO       = 'CO';         /** Operator: Contains */
/** @ignore */
const OPERATOR_NC       = 'NC';         /** Operator: Does Not Contain */
/** @ignore */
const OPERATOR_LIKE     = 'LIKE';       /** Operator: Like */
/** @ignore */
const OPERATOR_NOTLIKE  = 'NOTLIKE';    /** Operator: Not Like */
/** @ignore */
const OPERATOR_NE       = 'NE';         /** Operator: Not Equal */
/** @ignore */
const OPERATOR_TRUE     = 'TRUE';       /** Operator: True */
/** @ignore */
const OPERATOR_FALSE    = 'FALSE';      /** Operator: False */
/** @ignore */
const OPERATOR_NULL     = 'NULL';       /** Operator: Is Null */
/** @ignore */
const OPERATOR_IN       = 'IN';         /** Operator: In Set (comma separated list) */
/** @ignore */
const OPERATOR_NOT_IN   = 'NOT_IN';     /** Operator: Not In Set (comma separated list) */
/** @ignore */
const OPERATOR_SUBWHERE = 'SUBWHERE';   /** Operator: SUBWHERE */

/** Valid Operators */
/** @ignore */
const VALID_OPERATORS   = [
  OPERATOR_EQ,
  OPERATOR_GT,
  OPERATOR_GE,
  OPERATOR_LT,
  OPERATOR_LE,
  OPERATOR_CO,
  OPERATOR_NC,
  OPERATOR_LIKE,
  OPERATOR_NOTLIKE,
  OPERATOR_NE,
  OPERATOR_TRUE,
  OPERATOR_FALSE,
  OPERATOR_NULL,
  OPERATOR_IN,
  OPERATOR_NOT_IN,
  OPERATOR_SUBWHERE
];

/** Search Filter Constants */
/** @ignore */
const FILTER_SEARCH         = 'search';
/** @ignore */
const FILTER_SEARCH_AND     = 'search_AND';
/** @ignore */
const FILTER_SEARCH_OR      = 'search_OR';

/** Valid Filters */
/** @ignore */
const VALID_FILTERS         = [
    FILTER_SEARCH,
    FILTER_SEARCH_AND,
    FILTER_SEARCH_OR
];

/** 
 * Abstract request all *List_Load_Query api requests inherit from. 
 * @see https://docs.miva.com/json-api/list-load-query-overview
 */
class ListQueryRequest extends Request {
  /**
   * ListQueryRequest Constructor.
   */
  constructor(client = null) {
    super(client);
    this.sort                       = null;
    this.offset                     = 0;
    this.count                      = 0;
    this.filters                    = [];
    this.expressions                = [];
    this.onDemandColumns            = [];
    this.customFilters              = {};
    this.availableOnDemandColumns   = [];
    this.availableCustomFilters     = [];
    this.availableSortFields        = [];
    this.availableSearchFields      = [];
  }

  /**
   * @returns {string}
   * @const
   */
  static get SORT_ASCENDING() {
    return SORT_ASCENDING;
  }

  /**
   * @returns {string}
   * @const
   */
  static get SORT_DESCENDING() {
    return SORT_DESCENDING;
  }

  /**
   * Get the sorting field.
   * @returns {*}
   */
  getSort() {
    return this.sort;
  }

  /**
   * Set the sorting field.
   * @param {string} field
   * @param {string} direction
   * @returns {ListQueryRequest}
   */
  setSort(field, direction = SORT_ASCENDING) {
    direction = direction.toLowerCase();

    if (direction !== SORT_ASCENDING && direction !== SORT_DESCENDING) {
      direction = SORT_ASCENDING;
    }

    if (direction === SORT_DESCENDING && field.indexOf('-') !== 0) {
      field = '-' + field;
    }

    this.sort = field;

    return this;
  }

  /**
   * Get the available sorting fields for the request.
   * @returns {Array}
   */
  getAvailableSortFields() {
    return this.availableSortFields;
  }

  /**
   * Get the record offset.
   * @returns {*}
   */
  getOffset() {
    return this.offset;
  }

  /**
   * Set the record offset.
   * @param {number} offset
   * @returns {ListQueryRequest}
   */
  setOffset(offset) {
    this.offset = offset;
  }

  /**
   * Get the maximum records to request.
   * @returns {number}
   */
  getCount() {
    return this.count;
  }

  /**
   * Set the maximum records to request.
   * @param {number} count
   * @returns {ListQueryRequest}
   */
  setCount(count) {
    this.count = count;
    return this
  }

  /**
   * Get the available search fields for the request.
   * @returns {Array}
   */
  getAvailableSearchFields() {
    return this.availableSearchFields;
  }

  /**
   * Add an on demand column to the request.
   * @param {string|Array} column
   * @returns {ListQueryRequest}
   * @throws {Error}
   */
  addOnDemandColumn(column) {
    if (column.indexOf(':') === -1 && !util.inArray(column, this.availableOnDemandColumns)) {
      throw new Error(util.format('Column %s is not an on demand column. Available on demand columns are %s',
        column, this.availableOnDemandColumns.join(', ')));
    }

    if (!util.inArray(column, this.onDemandColumns)) {
      this.onDemandColumns.push(column);
    }

    return this
  }

  /**
   * Remove an on demand column from the request.
   * @param {string|Array} column
   * @returns {ListQueryRequest}
   */
  removeOnDemandColumn(column) {
    var i;
    var l;

    for (i = 0, l = this.onDemandColumns.length; i < l; i++) {
      if (column === this.onDemandColumns[i]) {
        this.onDemandColumns = this.onDemandColumns.splice(i, 1);
      }
    }

    return this
  }

  /**
   * Set the on demand columns to fetch.
   * @param {Array} columns
   * @returns {ListQueryRequest}
   */
  setOnDemandColumns(columns) {
    var i;
    var l;

    this.onDemandColumns = [];

    for (i = 0, l = columns.length; i < l; i++) {
      this.addOnDemandColumn(columns[i]);
    }

    return this
  }

  /**
   * Get the on demand columns to fetch.
   * @returns {Array}
   */
  getOnDemandColumns() {
    return this.onDemandColumns;
  }

  /**
   * Get the available on demand columns for the request.
   * @returns {Array}
   */
  getAvailableOnDemandColumns() {
    return this.availableOnDemandColumns;
  }

  /**
   * Get the custom filters to apply.
   * @returns {Array}
   */
  getCustomFilters() {
    return this.customFilters;
  }

  /**
   * Get the available custom filters for the request.
   * @returns {Array}
   */
  getAvailableCustomFilters() {
    return this.availableCustomFilters;
  }

  /**
   * Set the search filters to apply to the request.
   * @param {Array|FilterExpression} filters
   * @throws {Error}
   * @returns {ListQueryRequest}
   */
  setFilters(filters) {
    if (!util.isArray(filters) && !util.isInstanceOf(filters, FilterExpression)) {
      throw new Error('Expecting an array of instance of FilterExpression');
    }

    this.filters = filters;

    return this
  }

  /**
   * Get the search filters to apply to the request.
   * @returns {Array|FilterExpression}
   */
  getFilters() {
    return this.filters;
  }

  /**
   * Set a custom filter supported by the request.
   * @param {string} name The custom filter name
   * @param {*} value The custom filter value
   * @returns {ListQueryRequest}
   * @throws {Error}
   */
  setCustomFilter(name, value) {       
    var i;
    var l;

    if (util.isUndefined(this.availableCustomFilters[name])) {
      throw new Error(util.format('Invalid custom filter %s. Available filters are %s',
        name, Object.keys(this.availableCustomFilters).join(', ')));
    }

    if (util.isArray(this.availableCustomFilters[name])) {
      if (!util.inArray(value, this.availableOnDemandColumns[name])) {
        throw new Error(util.format('Invalid custom filter choice for %s. Available choices are %s',
          name, this.availableCustomFilters[name].join(', ')));            
      }
    }

    for (i = 0, l = this.customFilters.length; i < l; i++) {
      if (this.customFilters[i].name === name) {
        this.customFilters[i].value = value;
        return this;
      }
    }

    this.customFilters.push({
        name  : name,
        value : value
    });

    return this;
  }

  /**
   * Remove a custom filter applied to the request.
   * @param {string} name
   * @returns {ListQueryRequest}
   */
  removeCustomFilter(name) {
    var i;
    var l;

    for (i = 0, l = this.customFilters.length; i < l; i++) {
      if (this.customFilters[i].name === name) {
        this.customFilters = this.customFilters.splice(i, 1);
        break;
      }
    }

    return this;
  }

  /**
   * Creates a new FilterExpression object in the context the request.
   * @returns {FilterExpression}
   */
  filterExpression() {
    return new FilterExpression(this);
  }

  /**
   * Reduce the request to an Object.
   * @returns Object
   * @override
   */
  toObject() {
    var i;
    var l;
    
    var object = Object.assign(super.toObject(), {
      Sort    : this.sort,
      Offset  : this.offset,
      Count   : this.count,
      Filter  : []
    });

    if (util.isInstanceOf(this.filters, FilterExpression)) {
      var filters = this.filters.toArray();

      if (!util.isArray(filters)) {
        throw new Error(util.format('Expected filter expression to return an array but got %s',
          typeof(filters)));
      }

      if (filters.length) {
        object['Filter'] = filters;
      }
    } else if (util.isArray(this.filters) && this.filters.length) {
      object['Filter'] = this.filters;
    }

    if (this.onDemandColumns.length) {
      object['Filter'].push({
          name  : 'ondemandcolumns',
          value : this.onDemandColumns
      });
    }

    if (this.customFilters.length) {
      for (i = 0, l = this.customFilters.length; i < l; i++) {
        object['Filter'].push(this.customFilters[i]);
      }
    }

    return object;
  }

  /**
   * @override
   * @param {Object} data
   * @returns {Response}
   */
  createResponse(data) {
    return new ListQueryResponse(this, data);
  }
}

/** 
 * Abstract response all *List_Load_Query api responses inherit from.
 * @see https://docs.miva.com/json-api/list-load-query-overview
 */
class ListQueryResponse extends Response {
  /**
   * ListQueryResponse Constructor.
   * @param {Request} request
   * @param {Object} data
   */
  constructor(request, data = {}) {
    super(request, data);
  }

  /**
   * @returns {number}
   */
  getTotalCount() {
    return !util.isNullOrUndefined(this.data['data']['total_count']) ?
      this.data['data']['total_count'] : 0;
  }

  /**
   * @returns {number}
   */
  getStartOffset() {
    return !util.isNullOrUndefined(this.data['data']['start_offset']) ?
      this.data['data']['start_offset'] : 0;
  }
}

/** 
 * Filter Expresion 
 * @see https://docs.miva.com/json-api/list-load-query-overview#filter-list-parameters
 */
class FilterExpression {
  /**
   * FilterExpression Constructor.
   * @param {?Request} request
   */
  constructor(request = null) {
    this.request      = request;
    this.parent       = null;
    this.expressions  = [];
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_EQ() {
    return OPERATOR_EQ;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_GT() {
    return OPERATOR_GT;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_GE() {
    return OPERATOR_GE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_LT() {
    return OPERATOR_LT;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_LE() {
    return OPERATOR_LE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_CO() {
    return OPERATOR_CO;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_NC() {
    return OPERATOR_NC;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_LIKE() {
    return OPERATOR_LIKE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_NOTLIKE() {
    return OPERATOR_NOTLIKE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_NE() {
    return OPERATOR_NE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_TRUE() {
    return OPERATOR_TRUE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_FALSE() {
    return OPERATOR_FALSE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_NULL() {
    return OPERATOR_NULL;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_IN() {
    return OPERATOR_IN;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_NOT_IN() {
    return OPERATOR_NOT_IN;
  }

  /**
   * @returns {string}
   * @const
   */
  static get OPERATOR_SUBWHERE() {
    return OPERATOR_SUBWHERE;
  }

  /**
   * @returns {string}
   * @const
   */
  static get FILTER_SEARCH() {
    return FILTER_SEARCH;
  }

  /**
   * @returns {string}
   * @const
   */
  static get FILTER_SEARCH_AND() {
    return FILTER_SEARCH_AND;
  }

  /**
   * @returns {string}
   * @const
   */
  static get FILTER_SEARCH_OR() {
    return FILTER_SEARCH_OR;
  }

  /**
   * Get the parent expression.
   * @returns {?FilterExpression}
   */
  getParent() {
    return this.parent;
  }

  /**
   * Set the parent expression.
   * @param {?FilterExpression} parent
   * @returns {FilterExpression}
   */
  setParent(parent = null) {
    this.parent = parent;
    return this;
  }

  /**
   * Check if this expression is a child of another.
   *
   * @returns {boolean}
   */
  isChild() {
    return this.parent !== null;
  }

  /**
   * Get the child depth.
   * @returns {number}
   */
  childDepth() {
    var i = 0;

    var parent = this.getParent();

    while (parent) {
        parent = parent.getParent();
        i++;
    }

    return i;
  }

  /**
   * Create a new expression instance.
   * @returns FilterExpression
   */
  expr() {
    return new FilterExpression(this.request);
  }

  /**
   * Add a search filter.
   * @param {string} field
   * @param {string} operator
   * @param {*} value
   * @param {string} type
   * @returns {FilterExpression}
   */
  add(field, operator, value, type) {
    operator = operator.toUpperCase();

    if (this.request) {
      if (!util.inArray(field, this.request.getAvailableSearchFields())) {
        throw new Error(util.format('Field %s is invalid. Available fields are: %s',
          field, this.request.getAvailableSearchFields().join(', ')));
      }
    }

    if (!util.inArray(operator, VALID_OPERATORS)) {
      throw new Error(util.format('Operator %s is invalid. Available operators are: %s',
        field, VALID_OPERATORS.join(', ')));
    }

    if (!util.inArray(type, VALID_FILTERS)) {
      throw new Error(util.format('Filter type %s is invalid. Available search filter types are: %s',
        type, VALID_FILTERS.join(', ')));
    }

    if (!this.expressions.length) {
      type = FILTER_SEARCH;
    } else if (this.expressions.length && type === FILTER_SEARCH) {
      type = FILTER_SEARCH_OR;
    }

    if (type === FILTER_SEARCH || type === FILTER_SEARCH_AND) {
      this.expressions.push({
          type  : type,
          entry : new FilterExpressionEntry(field, operator, value, type)
      });
    } else if (type === FILTER_SEARCH_OR) {
      this.expressions.push({
          type  : type,
          entry : new FilterExpressionEntry(field, operator, value, type)
      });
    } else {
      throw new Error(util.format('Invalid type %s', type));
    }

    return this;
  }

  /**
   * Add a AND expression.
   * @param {FilterExpression} expression
   * @returns {FilterExpression}
   * @throws {Error}
   */
  andX(expression) {
    if (!util.isInstanceOf(expression, FilterExpression)) {
      throw new Error('Expecting an instance of FilterExpression');
    }

    expression.setParent(this);

    this.expressions.push({
        type  : FILTER_SEARCH_AND,
        entry : expression
    });

    return this;
  }

  /**
   * Add a OR expression.
   * @param {FilterExpression} expression
   * @returns {FilterExpression}
   * @throws {Error}
   */
  orX(expression) {
    if (!util.isInstanceOf(expression, FilterExpression)) {
      throw new Error('Expecting an instance of FilterExpression');
    }

    expression.setParent(this);

    this.expressions.push({
        type  : FILTER_SEARCH_OR,
        entry : expression
    });

    return this;
  }

  /**
   * Add a equal (x EQ y) filter for specified field.
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  equal(field, value) {
    return this.add(field, OPERATOR_EQ, value, FILTER_SEARCH);
  }

  /**
   * Add a equal (AND x EQ y) filter for specified field.
   *
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  andEqual(field, value) {
    return this.add(field, OPERATOR_EQ, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a equal (OR x EQ y) filter for specified field.
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  orEqual(field, value) {
      return this.add(field, OPERATOR_EQ, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a greater than (x GT y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  greaterThan(field, value) {
    return this.add(field, OPERATOR_GT, value, FILTER_SEARCH);
  }

  /**
   * Add a greater than (AND x GT y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  andGreaterThan(field, value) {
    return this.add(field, OPERATOR_GT, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a greater than (OR x GT y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  orGreaterThan(field, value) {
    return this.add(field, OPERATOR_GT, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a greater than or equal (x GE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  greaterThanEqual(field, value) {
    return this.add(field, OPERATOR_GE, value, FILTER_SEARCH);
  }

  /**
   * Add a greater than or equal (AND x GE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  andGreaterThanEqual(field, value) {
      return this.add(field, OPERATOR_GE, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a greater than or equal (OR x GE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  orGreaterThanEqual(field, value) {
    return this.add(field, OPERATOR_GE, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a less than (x LT y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  lessThan(field, value) {
    return this.add(field, OPERATOR_LT, value, FILTER_SEARCH);
  }

  /**
   * Add a less than (AND x LT y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  andLessThan(field, value) {
    return this.add(field, OPERATOR_LT, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a less than (OR x LT y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  orLessThan(field, value) {
    return this.add(field, OPERATOR_LT, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a less than or equal (x LE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  lessThanEqual(field, value) {
    return this.add(field, OPERATOR_LE, value, FILTER_SEARCH);
  }

  /**
   * Add a less than or equal (AND x LE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  andLessThanEqual(field, value) {
    return this.add(field, OPERATOR_LE, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a less than or equal (OR x LE y) filter for specified field.
   * @param {string} field
   * @param {number} value
   * @returns {FilterExpression}
   */
  orLessThanEqual(field, value) {
    return this.add(field, OPERATOR_LE, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a contains (x CO y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  contains(field, value) {
    return this.add(field, OPERATOR_CO, value, FILTER_SEARCH);
  }

  /**
   * Add a contains (AND x CO y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  andContains(field, value) {
    return this.add(field, OPERATOR_CO, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a contains (OR x CO y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  orContains(field, value) {
    return this.add(field, OPERATOR_CO, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a does not contains (x NC y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  doesNotContain(field, value) {
    return this.add(field, OPERATOR_NC, value, FILTER_SEARCH);
  }

  /**
   * Add a does not contains (AND x NC y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  andDoesNotContain(field, value) {
    return this.add(field, OPERATOR_NC, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a does not contains (OR x NC y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  orDoesNotContain(field, value) {
    return this.add(field, OPERATOR_NC, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a like (x LIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  like(field, value) {
    return this.add(field, OPERATOR_LIKE, value, FILTER_SEARCH);
  }

  /**
   * Add a like (AND x LIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  andLike(field, value) {
    return this.add(field, OPERATOR_LIKE, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a like (OR x LIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  orLike(field, value) {
      return this.add(field, OPERATOR_LIKE, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a not like (x NOTLIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  notLike(field, value) {
    return this.add(field, OPERATOR_NOTLIKE, value, FILTER_SEARCH);
  }

  /**
   * Add a not like (AND x NOTLIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  andNotLike(field, value) {
    return this.add(field, OPERATOR_NOTLIKE, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a not like (OR x NOTLIKE y) filter for specified field.
   * @param {string} field
   * @param {string} value
   * @returns {FilterExpression}
   */
  orNotLike(field, value) {
    return this.add(field, OPERATOR_NOTLIKE, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a not equal (x NE y) filter for specified field.
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  notEqual(field, value) {
    return this.add(field, OPERATOR_NE, value, FILTER_SEARCH);
  }

  /**
   * Add a not equal (AND x NE y) filter for specified field.
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  andNotEqual(field, value) {
    return this.add(field, OPERATOR_NE, value, FILTER_SEARCH_AND);
  }

  /**
   * Add a not equal (OR x NE y) filter for specified field.
   * @param {string} field
   * @param {string|number} value
   * @returns {FilterExpression}
   */
  orNotEqual(field, value) {
    return this.add(field, OPERATOR_NE, value, FILTER_SEARCH_OR);
  }

  /**
   * Add a true (x == true) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  isTrue(field) {
    return this.add(field, OPERATOR_TRUE, null, FILTER_SEARCH);
  }

  /**
   * Add a true (AND x == true) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  andIsTrue(field) {
    return this.add(field, OPERATOR_TRUE, null, FILTER_SEARCH_AND);
  }

  /**
   * Add a true (OR x == true) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  orIsTrue(field) {
    return this.add(field, OPERATOR_TRUE, null, FILTER_SEARCH_OR);
  }

  /**
   * Add a false (x == false) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  isFalse(field) {
    return this.add(field, OPERATOR_FALSE, null, FILTER_SEARCH);
  }

  /**
   * Add a false (AND x == false) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  andIsFalse(field) {
    return this.add(field, OPERATOR_FALSE, null, FILTER_SEARCH_AND);
  }

  /**
   * Add a false (OR x == false) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  orIsFalse(field) {
    return this.add(field, OPERATOR_FALSE, null, FILTER_SEARCH_OR);
  }

  /**
   * Add a is null (x == null) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  isNull(field) {
    return this.add(field, OPERATOR_NULL, null, FILTER_SEARCH);
  }

  /**
   * Add a is null (AND x == null) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  andIsNull(field) {
    return this.add(field, OPERATOR_NULL, null, FILTER_SEARCH_AND);
  }

  /**
   * Add a is null (OR x == null) filter for specified field.
   * @param {string} field
   * @returns {FilterExpression}
   */
  orIsNull(field) {
    return this.add(field, OPERATOR_NULL, null, FILTER_SEARCH_OR);
  }

  /**
   * Add a in (x IN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  isIn(field, values) {
    return this.add(field, OPERATOR_IN, values, FILTER_SEARCH);
  }

  /**
   * Add a in (AND x IN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  andIsIn(field, values) {
    return this.add(field, OPERATOR_IN, values, FILTER_SEARCH_AND);
  }

  /**
   * Add a in (OR x IN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  orIsIn(field, values) {
    return this.add(field, OPERATOR_IN, values, FILTER_SEARCH_OR);
  }

  /**
   * Add a not in (x NOTIN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  notIn(field, values) {
    return this.add(field, OPERATOR_NOT_IN, values, FILTER_SEARCH);
  }

  /**
   * Add a not in (AND x NOTIN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  andNotIn(field, values) {
    return this.add(field, OPERATOR_NOT_IN, values, FILTER_SEARCH_AND);
  }

  /**
   * Add a not in (OR x NOTIN y,z,.. ) filter for specified field.
   * @param {string} field
   * @param {Array|string} values
   * @returns {FilterExpression}
   */
  orNotIn(field, values) {
    return this.add(field, OPERATOR_NOT_IN, values, FILTER_SEARCH_OR);
  }

  /**
   * Reduce the built expression(s) to an array.
   * @returns {Array}
   */
  toArray() {
    var i;
    var l;
    var entry;
    var ret = [];

    for (i = 0, l = this.expressions.length; i < l; i++) {
        entry = {};

      if (util.isInstanceOf(this.expressions[i]['entry'], FilterExpression)) {
        entry = {
          name  : this.expressions[i]['type'],
          value : this.expressions[i]['entry'].toArray()
        };
      } else {
        if (this.isChild()) {
          entry = {
              field: this.expressions[i]['type'],
              operator: 'SUBWHERE',
              value: [
                {
                  field     : this.expressions[i]['entry'].getLeft(),
                  operator  : this.expressions[i]['entry'].getOperator(),
                  value     : util.isArray(this.expressions[i]['entry'].getRight()) ?
                    this.expressions[i]['entry'].getRight().join(',') : this.expressions[i]['entry'].getRight()
                },
              ]
          };
        } else {
          entry = {
            name  : this.expressions[i]['type'],
            value : [
              {
                field     : this.expressions[i]['entry'].getLeft(),
                operator  : this.expressions[i]['entry'].getOperator(),
                value     : util.isArray(this.expressions[i]['entry'].getRight()) ?
                  this.expressions[i]['entry'].getRight().join(',') : this.expressions[i]['entry'].getRight()
              }
            ]
          };
        }
      }

      ret.push(entry);
    }

    return ret;
  }
}

/** Filter Expression Entry */
class FilterExpressionEntry {
  /**
   * FilterExpressionEntry Constructor.
   * @param left
   * @param operator
   * @param right
   * @param search
   */
  constructor(left, operator, right, search = 'search') {
    this.left       = left;
    this.operator   = operator;
    this.right      = right;
    this.search     = search;
  }

  /**
   * Get the left side of the expression.
   * @returns {string}
   */
  getLeft() {
    return this.left;
  }

  /**
   * Set the left side of the comparison.
   * @param {string} left
   * @returns {FilterExpressionEntry}
   */
  setLeft(left) {
    this.left = left;
    return this;
  }

  /**
   * Get the expression operator.
   * @returns {string}
   */
  getOperator() {
    return this.operator;
  }

  /**
   * Set the expression operator.
   * @param {string} operator
   * @returns {FilterExpressionEntry}
   */
  setOperator(operator) {
    this.operator = operator;
    return this;
  }

  /**
   * Get the right side of the expression.
   * @returns {string}
   */
  getRight() {
    return this.right;
  }

  /**
   * Set the right side of the expression.
   * @param {string} right
   * @returns {FilterExpressionEntry}
   */
  setRight(right) {
    this.right = right;
    return this;
  }
}

module.exports = {
  ListQueryRequest,
  ListQueryResponse,
  FilterExpression,
  FilterExpressionEntry
};