OpenVeo server API for plugins

API Docs for: 7.0.0
Show:

File: lib/storages/ResourceFilter.js

'use strict';

/**
 * @module storages
 */

/**
 * Defines a storage filter.
 *
 * A filter is a uniform way of filtering results common to all storages.
 * A filter can contain only one "or" operation, one "nor" operation and one "and" operation.
 *
 *     var filter = new ResourceFilter()
 *     .equal('field1', 42)
 *     .notEqual('field2', 42)
 *     .greaterThan('field3', 42)
 *     .greaterThanEqual('field4', 42)
 *     .in('field5', [42])
 *     .lesserThan('field6', 42)
 *     .lesserThanEqual('field7', 42)
 *     .regex('field8', /^Something/i)
 *     .search('query')
 *     .or([
 *       new ResourceFilter().equal('field8', 42),
 *       new ResourceFilter().notIn('field9', [42])
 *     ])
 *     .nor([
 *       new ResourceFilter().equal('field10', 42),
 *       new ResourceFilter().notIn('field11', [42])
 *     )],
 *     .and([
 *       new ResourceFilter().equal('field12', 42),
 *       new ResourceFilter().notIn('field13', [42])
 *     )];
 *
 * @class ResourceFilter
 * @constructor
 */
function ResourceFilter() {
  Object.defineProperties(this, {

    /**
     * The list of operations.
     *
     * @property operations
     * @type Array
     * @final
     */
    operations: {
      value: []
    }

  });

}

module.exports = ResourceFilter;

/**
 * The available operators.
 *
 * @property OPERATORS
 * @type Object
 * @final
 * @static
 */
ResourceFilter.OPERATORS = {
  OR: 'or',
  NOR: 'nor',
  AND: 'and',
  EQUAL: 'equal',
  NOT_EQUAL: 'notEqual',
  IN: 'in',
  NOT_IN: 'notIn',
  GREATER_THAN: 'greaterThan',
  GREATER_THAN_EQUAL: 'greaterThanEqual',
  LESSER_THAN: 'lesserThan',
  LESSER_THAN_EQUAL: 'lesserThanEqual',
  REGEX: 'regex',
  SEARCH: 'search'
};
Object.freeze(ResourceFilter.OPERATORS);

/**
 * Validates the type of a value.
 *
 * @method isValidType
 * @private
 * @param {String} value The value to test
 * @param {Array} The list of authorized types as strings
 * @return {Boolean} true if valid, false otherwise
 */
function isValidType(value, authorizedTypes) {
  var valueToString = Object.prototype.toString.call(value);

  for (var i = 0; i < authorizedTypes.length; i++)
    if (valueToString === '[object ' + authorizedTypes[i] + ']') return true;

  return false;
}

/**
 * Adds a comparison operation to the filter.
 *
 * @method addComparisonOperation
 * @private
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @param {String} Resource filter operator
 * @param {Array} The list of authorized types as strings
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
function addComparisonOperation(field, value, operator, authorizedTypes) {
  if (!isValidType(field, ['String'])) throw new TypeError('Invalid field');
  if (!isValidType(value, authorizedTypes)) throw new TypeError('Invalid value');

  this.operations.push({
    type: operator,
    field: field,
    value: value
  });
  return this;
}

/**
 * Adds a logical operation to the filter.
 *
 * Only one logical operation can be added in a filter.
 *
 * @method addLogicalOperation
 * @private
 * @param {Array} filters The list of filters
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
function addLogicalOperation(filters, operator) {
  for (var i = 0; i < filters.length; i++)
    if (!(filters[i] instanceof ResourceFilter)) throw new TypeError('Invalid filters');

  if (this.hasOperation(operator)) {

    // This logical operator already exists in the list of operations
    // Just add the new filters to the operator

    for (var operation of this.operations) {
      if (operation.type === operator)
        operation.filters = operation.filters.concat(filters);
    }

  } else {

    // This logical operator does not exist yet in the list of operations

    this.operations.push({
      type: operator,
      filters: filters
    });
  }

  return this;
}

/**
 * Adds an equal operation to the filter.
 *
 * @method equal
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.equal = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.EQUAL,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds a "not equal" operation to the filter.
 *
 * @method equal
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.notEqual = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.NOT_EQUAL,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds a "greater than" operation to the filter.
 *
 * @method greaterThan
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.greaterThan = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.GREATER_THAN,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds a "greater than or equal" operation to the filter.
 *
 * @method greaterThanEqual
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.greaterThanEqual = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.GREATER_THAN_EQUAL,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds a "lesser than" operation to the filter.
 *
 * @method lesserThan
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.lesserThan = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.LESSER_THAN,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds a "lesser than equal" operation to the filter.
 *
 * @method lesserThanEqual
 * @param {String} field The name of the field
 * @param {String|Number|Boolean|Date} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.lesserThanEqual = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.LESSER_THAN_EQUAL,
    ['String', 'Boolean', 'Date', 'Number']
  );
};

/**
 * Adds an "in" operation to the filter.
 *
 * @method in
 * @param {String} field The name of the field
 * @param {Array} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.in = function(field, value) {
  if (!isValidType(value, ['Array'])) throw new TypeError('Invalid value');

  for (var i = 0; i < value.length; i++)
    if (!isValidType(value[i], ['String', 'Boolean', 'Date', 'Number'])) throw new TypeError('Invalid value');

  return addComparisonOperation.call(this, field, value, ResourceFilter.OPERATORS.IN, ['Array']);
};

/**
 * Adds a "not in" operation to the filter.
 *
 * @method in
 * @param {String} field The name of the field
 * @param {Array} value The value to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.notIn = function(field, value) {
  if (!isValidType(value, ['Array'])) throw new TypeError('Invalid value');

  for (var i = 0; i < value.length; i++)
    if (!isValidType(value[i], ['String', 'Boolean', 'Date', 'Number'])) throw new TypeError('Invalid value');

  return addComparisonOperation.call(this, field, value, ResourceFilter.OPERATORS.NOT_IN, ['Array']);
};

/**
 * Adds a "regular expression" operation to the filter.
 *
 * @method regex
 * @param {String} field The name of the field
 * @param {RegExp} value The regular expression to compare the field to
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if field and / or value is not valid
 */
ResourceFilter.prototype.regex = function(field, value) {
  return addComparisonOperation.call(
    this,
    field,
    value,
    ResourceFilter.OPERATORS.REGEX,
    ['RegExp']
  );
};

/**
 * Adds a "or" operation to the filter.
 *
 * @method or
 * @param {Array} filters The list of filters
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if filters are not valid
 */
ResourceFilter.prototype.or = function(filters) {
  return addLogicalOperation.call(this, filters, ResourceFilter.OPERATORS.OR);
};

/**
 * Adds a "nor" operation to the filter.
 *
 * @method nor
 * @param {Array} filters The list of filters
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if filters are not valid
 */
ResourceFilter.prototype.nor = function(filters) {
  return addLogicalOperation.call(this, filters, ResourceFilter.OPERATORS.NOR);
};

/**
 * Adds an "and" operation to the filter.
 *
 * @method and
 * @param {Array} filters The list of filters
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if filters are not valid
 */
ResourceFilter.prototype.and = function(filters) {
  return addLogicalOperation.call(this, filters, ResourceFilter.OPERATORS.AND);
};

/**
 * Adds a "search" operation to the filter.
 *
 * @method search
 * @param {String} value The search query
 * @return {ResourceFilter} The actual filter
 * @throws {TypeError} An error if value is not a String
 */
ResourceFilter.prototype.search = function(value) {
  if (!isValidType(value, ['String'])) throw new TypeError('Invalid value');

  this.operations.push({
    type: ResourceFilter.OPERATORS.SEARCH,
    value: value
  });

  return this;
};

/**
 * Tests if an operation has already been specified.
 *
 * @method hasOperation
 * @param {String} operator Operation operator
 * @return {Boolean} true if the operation has already been added to this filter, false otherwise
 */
ResourceFilter.prototype.hasOperation = function(operator) {
  for (var i = 0; i < this.operations.length; i++)
    if (this.operations[i].type === operator) return true;

  return false;
};

/**
 * Gets a logical operation from filter.
 *
 * @method getLogicalOperation
 * @param {String} operator Logical operator to look for
 * @return {Object|Null} The operation with:
 * -**String** type The operation type
 * -**Array** filters The list of filters inside the logical operation
 */
ResourceFilter.prototype.getLogicalOperation = function(operator) {
  for (var i = 0; i < this.operations.length; i++) {
    if (this.operations[i].type === operator) return this.operations[i];

    if ([
      ResourceFilter.OPERATORS.OR,
      ResourceFilter.OPERATORS.NOR,
      ResourceFilter.OPERATORS.AND
    ].indexOf(this.operations[i].type) >= 0) {
      for (var j = 0; j < this.operations[i].filters.length; j++) {
        var result = this.operations[i].filters[j].getLogicalOperation(operator);
        if (result) return result;
      }
    }
  }

  return null;
};

/**
 * Gets an operation from filter or sub filters.
 *
 * @method getComparisonOperation
 * @param {String} operator Operation operator
 * @param {String} field Operation field
 * @return {Object|Null} The operation with:
 * -**String** type The operation type
 * -**String** field The operation field
 * -**String|Number|Boolean|Date|Array** value The operation value
 */
ResourceFilter.prototype.getComparisonOperation = function(operator, field) {
  for (var i = 0; i < this.operations.length; i++) {
    if ([
      ResourceFilter.OPERATORS.OR,
      ResourceFilter.OPERATORS.NOR,
      ResourceFilter.OPERATORS.AND
    ].indexOf(this.operations[i].type) >= 0) {
      for (var j = 0; j < this.operations[i].filters.length; j++) {
        var result = this.operations[i].filters[j].getComparisonOperation(operator, field);
        if (result) return result;
      }
    } else if (this.operations[i].type === operator && (!field || this.operations[i].field === field))
      return this.operations[i];
  }

  return null;
};