API Docs for:
Show:

File: odata-query-generator.js

/**
 * The OData module is designed to generate OData query strings for specific
 * API endpoints. General usage is as follows.
 *
 * <pre>
 * var qb = new OData.QueryBuilder('/api/users');
 * qb.addWhereFilter('id1', OData.STRING, 'fName', OData.EQUALS, 'Bartholomew');
 * var query = qb.generateQueryUrl(); // query would equal "/api/users/?$filter=fName eq 'Bartholomew'"
 * </pre>
 *
 * The OData object provides the following constants for use with `QueryBuilder.addWhereFilter()`.
 * <br/>
 * Filter types:
 * - OData.NULL
 * - OData.BOOLEAN
 * - OData.DECIMAL
 * - OData.SINGLE
 * - OData.DOUBLE
 * - OData.BYTE
 * - OData.SBYTE
 * - OData.INT16
 * - OData.INT32
 * - OData.INT64
 * - OData.TIME
 * - OData.DATE_TIME
 * - OData.DATE_TIME_OFFSET
 * - OData.GUID
 * - OData.STRING
 *
 * Query Operators:
 * - OData.IS_TRUE
 * - OData.IS_FALSE
 * - OData.ROUND_EQUALS
 * - OData.FLOOR_EQUALS
 * - OData.CEILING_EQUALS
 * - OData.EQUALS
 * - OData.NOT_EQUALS
 * - OData.GREATER_THAN
 * - OData.GREATER_THAN_OR_EQUAL_TO
 * - OData.LESS_THAN
 * - OData.LESS_THAN_OR_EQUAL_TO
 * - OData.BEFORE
 * - OData.AFTER
 * - OData.YEAR_EQUALS
 * - OData.MONTH_NUMBER_EQUALS
 * - OData.DAY_NUMBER_EQUALS
 * - OData.HOUR_EQUALS
 * - OData.MINUTE_EQUALS
 * - OData.SECOND_EQUALS
 * - OData.IN_SEMICOLON_SEPARATED
 * - OData.CASE_INSENSITIVE_EQUALS
 * - OData.CASE_INSENSITIVE_NOT_EQUALS
 * - OData.STARTS_WITH
 * - OData.DOES_NOT_START_WITH
 * - OData.ENDS_WITH
 * - OData.DOES_NOT_END_WITH
 * - OData.CONTAINS
 * - OData.HAS_LENGTH
 *
 * The OData object provides the following constants for use with `QueryBuilder.setOrderBy()`.
 * <br/>
 * OrderBy Orders:
 * - OData.NONE
 * - OData.ASC
 * - OData.DESC
 *
 * @module OData
 */
 define(function() {
  var OData = {};

  // ----------------------------------------------------------------------------
  //    Constants
  // ----------------------------------------------------------------------------
  // Filter types
  OData.NULL = 'NULL';
  OData.BOOLEAN = 'BOOLEAN';
  OData.DECIMAL = 'DECIMAL';
  OData.SINGLE = 'SINGLE';
  OData.DOUBLE = 'DOUBLE';
  OData.BYTE = 'BYTE';
  OData.SBYTE = 'SBYTE';
  OData.INT16 = 'INT16';
  OData.INT32 = 'INT32';
  OData.INT64 = 'INT64';
  OData.TIME = 'TIME';
  OData.DATE_TIME = 'DATE_TIME';
  OData.DATE_TIME_OFFSET = 'DATE_TIME_OFFSET';
  OData.GUID = 'GUID';
  OData.STRING = 'STRING';
  // Query Operators
  OData.IS_TRUE = 'IS_TRUE';
  OData.IS_FALSE = 'IS_FALSE';
  OData.ROUND_EQUALS = 'ROUND_EQUALS';
  OData.FLOOR_EQUALS = 'FLOOR_EQUALS';
  OData.CEILING_EQUALS = 'CEILING_EQUALS';
  OData.EQUALS = 'EQUALS';
  OData.NOT_EQUALS = 'NOT_EQUALS';
  OData.GREATER_THAN = 'GREATER_THAN';
  OData.GREATER_THAN_OR_EQUAL_TO = 'GREATER_THAN_OR_EQUAL_TO';
  OData.LESS_THAN = 'LESS_THAN';
  OData.LESS_THAN_OR_EQUAL_TO = 'LESS_THAN_OR_EQUAL_TO';
  OData.BEFORE = 'BEFORE';
  OData.AFTER = 'AFTER';
  OData.YEAR_EQUALS = 'YEAR_EQUALS';
  OData.MONTH_NUMBER_EQUALS = 'MONTH_NUMBER_EQUALS';
  OData.DAY_NUMBER_EQUALS = 'DAY_NUMBER_EQUALS';
  OData.HOUR_EQUALS = 'HOUR_EQUALS';
  OData.MINUTE_EQUALS = 'MINUTE_EQUALS';
  OData.SECOND_EQUALS = 'SECOND_EQUALS';
  OData.IN_SEMICOLON_SEPARATED = 'IN_SEMICOLON_SEPARATED';
  OData.CASE_INSENSITIVE_EQUALS = 'CASE_INSENSITIVE_EQUALS';
  OData.CASE_INSENSITIVE_NOT_EQUALS = 'CASE_INSENSITIVE_NOT_EQUALS';
  OData.STARTS_WITH = 'STARTS_WITH';
  OData.DOES_NOT_START_WITH = 'DOES_NOT_START_WITH';
  OData.ENDS_WITH = 'ENDS_WITH';
  OData.DOES_NOT_END_WITH = 'DOES_NOT_END_WITH';
  OData.CONTAINS = 'CONTAINS';
  OData.HAS_LENGTH = 'HAS_LENGTH';
  // OrderBy Orders
  OData.NONE = 0;
  OData.ASC = 1;
  OData.DESC = 2;

  /**
   * Extend the built in String class with a format function.
   * @private
   * @method format
   */
  if (!String.prototype.format) {
    String.prototype.format = function() {
      var args = arguments;
      return this.replace(/{(\d+)}/g, function(match, number) {
        return typeof args[number] !== 'undefined' ? args[number] : match;
      });
    };
  }

  /**
   * Gives the child object a copy of the parent object's prototype.
   * @private
   * @method _extend
   * @param base {Function} The base method whose prototype will be copied.
   * @param child {Function} The child method who will get a copy of the parent's prototype.
   * @return {Function} The augmented child method.
   */
  OData._extend = function(base, child) {
    child.prototype = new base();
    child.prototype.constructor = child;
    child.base = base.prototype;
    return child;
  };

  /**
   * Make sure there is a trailing /.
   * @private
   * @method _cleanEndpointUrl
   * @param url {String} The endpoint url.
   * @return {String} The cleaned endpoint url.
   */
  OData._cleanEndpointUrl = function(url) {
    if (url[url.length - 1] !== '/') {
      url += '/';
    }
    return url;
  };


  // ------------------------------------------------------------------------------
  // The filters used to make meaningful queries to the service.
  // ------------------------------------------------------------------------------
  /**
   * Where clause filter options base class.
   * @class FilterOptions
   * @constructor
   */
  OData.FilterOptions = function() {
    this.options = {
      encodeUrlComponents: false
    };
    this.values = [];
  };

  /**
   * Where clause filter base class init method.
   * @method init
   * @param options {Object} Options to set on each where filter.
   */
  OData.FilterOptions.prototype.init = function(options) {
    for (var name in options) {
      this.options[name] = options[name];
    }
  };

  /**
   * Base query string for each type of filter.
   * @method getWhereQuery
   * @param propNames {Array|String} The list of property names.
   * @param operator {String} The comparator for this filter.
   * @param value {Array|String} The value of the property.
   * @return {String} The query string for the specified where filter.
   */
  OData.FilterOptions.prototype.getWhereQuery = function(propNames, operator, values) {
    var i,
        l,
        finalQuery = [],
        filter = this.values[operator],
        query = '';
    if (propNames && values !== undefined && values !== null) {
      if (Array.isArray(propNames) && Array.isArray(values)) {
        if (propNames.length > values.length) {
          // Too many properties, truncate to match values
          propNames.splice(values.length, propNames.length);
        }
        if (values.length > propNames.length) {
          // Too many values, truncate to match properties
          values.splice(propNames.length, values.length);
        }
        for(i=0, l=propNames.length; i < l; i++) {
          finalQuery.push(filter.stringFormat.format(propNames[i], values[i]));
        }
        query = finalQuery.join(' or ');
      } else if (Array.isArray(propNames) || Array.isArray(values)) {
        // Is propNames an array
        if (Array.isArray(propNames)) {
          // make all the props match the one value
          for(i=0, l=propNames.length; i < l; i++) {
           finalQuery.push(filter.stringFormat.format(propNames[i], values));
          }
        } else {
          // make all the values match the one propertyName
          for(i=0, l=values.length; i < l; i++) {
            finalQuery.push(filter.stringFormat.format(propNames, values[i]));
          }
        }
        query = finalQuery.join(' or ');
      } else {
        // We have only one property/value.
        query = filter.stringFormat.format(propNames, values);
      }
    }
    return query;
  };

  /**
   * Null where clause filter class.
   * @class NullFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.NullFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = [
      { errorMessage: 'You are not able to query on this property.' }
    ];
  });

  /**
   * Gets the where query, which for null is an empty string.
   * @method getWhereQuery
   * @return {String} An empty string.
   */
  OData.NullFilterOptions.prototype.getWhereQuery = function() {
    return '';
  };

  /**
   * Boolean where clause filter class.
   * @class BooleanFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.BooleanFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'IS_TRUE':  { stringFormat: '{0} eq true'  },
      'IS_FALSE': { stringFormat: '{0} eq false' }
    };
  });

  /**
   * Gets the where query for Boolean objects.
   * @method getWhereQuery
   * @param propNames {Array|String} The list of property names.
   * @param operator {String} The type of comparator to perform.
   * @return {String} A Boolean query string.
   */
  OData.BooleanFilterOptions.prototype.getWhereQuery = function(propNames, operator) {
    return OData.BooleanFilterOptions.base.getWhereQuery.call(
      this, propNames, operator, '');
  };

  /**
   * FloatingPoint where clause filter class.
   * @class FloatingPointFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.FloatingPointFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'ROUND_EQUALS':             { stringFormat: 'round({0}) eq {1}'   },
      'FLOOR_EQUALS':             { stringFormat: 'floor({0}) eq {1}'   },
      'CEILING_EQUALS':           { stringFormat: 'ceiling({0}) eq {1}' },
      'EQUALS':                   { stringFormat: '{0} eq {1}'          },
      'NOT_EQUALS':               { stringFormat: '{0} ne {1}'          },
      'GREATER_THAN':             { stringFormat: '{0} gt {1}'          },
      'GREATER_THAN_OR_EQUAL_TO': { stringFormat: '{0} ge {1}'          },
      'LESS_THAN':                { stringFormat: '{0} lt {1}'          },
      'LESS_THAN_OR_EQUAL_TO':    { stringFormat: '{0} le {1}'          }
    };
  });

  /**
   * Integer where clause filter class.
   * @class IntegerFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.IntegerFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'EQUALS':                   { stringFormat: '{0} eq {1}' },
      'NOT_EQUALS':               { stringFormat: '{0} ne {1}' },
      'GREATER_THAN':             { stringFormat: '{0} gt {1}' },
      'GREATER_THAN_OR_EQUAL_TO': { stringFormat: '{0} ge {1}' },
      'LESS_THAN':                { stringFormat: '{0} lt {1}' },
      'LESS_THAN_OR_EQUAL_TO':    { stringFormat: '{0} le {1}' }
    };
  });

  /**
   * Date and time where clause filter class.
   * @class DateTimeFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.DateTimeFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'BEFORE':              { stringFormat: "{0} le datetime'{1}'" },
      'AFTER':               { stringFormat: "{0} ge datetime'{1}'" },
      'YEAR_EQUALS':         { stringFormat: 'year({0}) eq {1}'     },
      'MONTH_NUMBER_EQUALS': { stringFormat: 'month({0}) eq {1}'    },
      'DAY_NUMBER_EQUALS':   { stringFormat: 'day({0}) eq {1}'      },
      'HOUR_EQUALS':         { stringFormat: 'hour({0}) eq {1}'     },
      'MINUTE_EQUALS':       { stringFormat: 'minute({0}) eq {1}'   },
      'SECOND_EQUALS':       { stringFormat: 'second({0}) eq {1}'   }
    };
  });

  /**
   * Gets the where query for DateTime objects.
   * @method getWhereQuery
   * @param propNames {Array|String} The list of property names.
   * @param operator {Array} The type of comparator to perform.
   * @param values {Array|String} The value of the property.
   * @return {String} A DateTime query string.
   */
  OData.DateTimeFilterOptions.prototype.getWhereQuery = function(propNames, operator, values) {
    var query = '',
        val,
        date,
        i,
        validDate = function(value) {
          return (Object.prototype.toString.call(value) === '[object Date]') ? true : false;
        };
    // Take values and convert to ISO string dates
    if (Array.isArray(values)) {
      i = values.length - 1;
      while (i >= 0) {
        val = values[i];
        if (validDate(val)) {
          values[i] = val.toISOString();
        } else {
          date = new Date(val);
          if (validDate(date)) {
            values[i] = date.toISOString();
          } else {
            // Remove the value as it wasn't able to convert to a date
            console.error('OData.DateTimeFilterOptions.getWhereQuery(): Could not convert ' +
              val.toString() +
              ' to a date. Removing from query');
            values.splice(i,1);
          }
        }
        i--;
      }
    } else {
      if (validDate(values)) {
        values = new Date(values).toISOString();
      } else {
        date = new Date(values);
        if (validDate(date)) {
          values = date.toISOString();
        } else {
          // Dropout because `values` wasn't able to convert to a date
          console.error('OData.DateTimeFilterOptions.getWhereQuery(): Could not convert ' +
            values.toString() + ' to a date. Query was not generated.');
          return;
        }
      }
    }
    query = OData.DateTimeFilterOptions.base.getWhereQuery.call(
      this, propNames, operator, values);
      return query;
  };

  /**
   * GUID where clause filter class.
   * @class GuidFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.GuidFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'EQUALS':     { stringFormat: "{0} eq guid'{1}'" },
      'NOT_EQUALS': { stringFormat: "{0} ne guid'{1}'" }
    };
  });

  /**
   * String where clause filter class.
   * @class StringFilterOptions
   * @extends FilterOptions
   * @param options {Object} The options object.
   */
  OData.StringFilterOptions = OData._extend(OData.FilterOptions, function(options) {
    this.init(options);
    this.values = {
      'EQUALS':                      { stringFormat: "{0} eq '{1}'"                    },
      'NOT_EQUALS':                  { stringFormat: "{0} ne '{1}'"                    },
      'IN_SEMICOLON_SEPARATED':      { stringFormat: "{0} eq '{1}'"                    },
      'CASE_INSENSITIVE_EQUALS':     { stringFormat: "tolower({0}) eq tolower('{1}')"  },
      'CASE_INSENSITIVE_NOT_EQUALS': { stringFormat: "tolower({0}) eq tolower('{1}')"  },
      'STARTS_WITH':                 { stringFormat: "startswith({0}, '{1}') eq true"  },
      'DOES_NOT_START_WITH':         { stringFormat: "startswith({0}, '{1}') eq false" },
      'ENDS_WITH':                   { stringFormat: "endswith({0}, '{1}') eq true"    },
      'DOES_NOT_END_WITH':           { stringFormat: "endswith({0}, '{1}') eq false"   },
      'CONTAINS':                    { stringFormat: "substringof('{1}', {0}) eq true" },
      'HAS_LENGTH':                  { stringFormat: "length({0}) eq {1}"              }
    };
  });

  /**
   * Gets the where query for String objects.
   * @method getWhereQuery
   * @param propNames {Array|String} The list of property names.
   * @param operator {String} The type of comparator to perform.
   * @param values {Array|String} The value of the property.
   * @return {String} A String query string.
   */
  OData.StringFilterOptions.prototype.getWhereQuery = function(propNames, operator, values) {
    var query = '',
        i,
        l,
        segments,
        finalValue;
    // Perform some sanitization
    if (Array.isArray(values)) {
      for (i=0, l=values.length; i < l; i++) {
        values[i] = String(values[i]).replace(new RegExp("'", 'g'), "''");
        if (this.options.encodeUrlComponents) {
          values[i] = encodeURIComponent(values[i]);
        }
      }
    } else {
      values = String(values).replace(new RegExp("'", 'g'), "''");
      if (this.options.encodeUrlComponents) {
        values = encodeURIComponent(values);
      }
    }
    if (operator === OData.IN_SEMICOLON_SEPARATED) {
      segments = values.split(';');
      finalValue = [];
      for (i=0, l=segments.length; i < l; i++) {
        finalValue.push(OData.StringFilterOptions.base.getWhereQuery.call(
          this, propNames, operator, segments[i].trim()));
      }
      query = finalValue.join(' or ');
    } else {
      query = OData.StringFilterOptions.base.getWhereQuery.call(
        this, propNames, operator, values);
    }
    return query;
  };

  /**
   * Where clause filter class.
   * @class WhereFilterOptions
   * @param options {Object} The options object.
   */
  OData.WhereFilterOptions = function(options) {
    this.NULL = new OData.NullFilterOptions(options);
    this.BOOLEAN = new OData.BooleanFilterOptions(options);
    this.DECIMAL =
        this.SINGLE =
        this.DOUBLE = new OData.FloatingPointFilterOptions(options);
    this.BYTE =
        this.SBYTE =
        this.INT16 =
        this.INT32 =
        this.INT64 = new OData.IntegerFilterOptions(options);
    this.TIME =
        this.DATE_TIME =
        this.DATE_TIME_OFFSET = new OData.DateTimeFilterOptions(options);
    this.GUID = new OData.GuidFilterOptions(options);
    this.STRING = new OData.StringFilterOptions(options);
  };

  /**
   * Where clause filter class.
   * @method getFilterHandler
   * @param type {String} Type of filter to retrieve.
   * @return {Object} A filter object.
   */
  OData.WhereFilterOptions.prototype.getFilterHandler = function(type) {
    if (this[type]) {
      return this[type];
    } else {
      return this.Null;
    }
  };

  // -----------------------------------------------------------------------------------
  // The query builder class, which knows everything about entities, properties, etc.
  // -----------------------------------------------------------------------------------

  /**
   * Query builder class.
   * @class QueryBuilder
   * @param urlEndpoint {String} The URL of the service endpoint.
   * @param options {Object} The options object.
   */
  OData.QueryBuilder = function(urlEndpoint, options) {
    if (!urlEndpoint) {
      throw 'You must specify the OData service endpoint URL. This should be everything up to the "?"';
    }
    this.options = options || {};
    this.baseUrl = OData._cleanEndpointUrl(urlEndpoint);
    this.top = null;
    this.skip = null;
    this.whereFilters = [];
    this.orderBy = [];
    this.filterOptions = new OData.WhereFilterOptions(this.options);
  };

  /**
   * Set the $top value in the final query. If you set val to null,
   * $top will be removed from the final query string.
   * @method setTop
   * @param val {Number} The top value.
   */
  OData.QueryBuilder.prototype.setTop = function(val) {
    this.top = isNaN(parseInt(val)) ? null : parseInt(val);
  };

  /**
   * Set the $skip value in the final query. If you set val to null,
   * $skip will be removed from the final query string.
   * @method setSkip
   * @param val {Number} The skip value.
   */
  OData.QueryBuilder.prototype.setSkip = function(val) {
    this.skip = isNaN(parseInt(val)) ? null : parseInt(val);
  };

  /**
   * Set the $orderby value in the final query. You need to pass the
   * name of the property you want to sort by, and the order of the
   * sort. Valid values are 0, 1, or 2 (NONE, ASC, and DESC respectively).
   * If you pass null, $orderby will be removed from the final query string.
   * @method setOrderBy
   * @param propName {String} The property to order by.
   * @param val {Number} The sort order.
   */
  OData.QueryBuilder.prototype.setOrderBy = function(propName, val) {
    if (propName && val && (val === 0 || val === 1 || val === 2)) {
      this.orderBy.push({
        propName: propName,
        value: val
      });
    } else {
      this.orderBy.length = 0;
      console.warn('OData.QueryBuilder::setOrderBy(): Missing property name or invalid sort order. $orderBy will be ignored.');
    }
  };

  /**
   * Return the base endpoint url.
   * @method getBaseUrl
   * @return {String} The base endpoint url.
   */
  OData.QueryBuilder.prototype.getBaseUrl = function() {
    return this.baseUrl;
  };

  /**
   * Return the final OData query URL.
   * @method generateQueryUrl
   * @return {String} A fully qualified query URL.
   */
  OData.QueryBuilder.prototype.generateQueryUrl = function() {
    var url = this.getBaseUrl(),
        i,
        propertyId,
        propertyName,
        value,
        queryFiltersString,
        sortingOptions = [],
        lastUrlCharIndex;
    url += '?';
    if (this.skip !== undefined && this.skip !== null) {
      url += '$skip=' + this.skip + '&';
    }
    if (this.top !== undefined && this.top !== null) {
      url += '$top=' + this.top + '&';
    }
    if (this.whereFilters && this.whereFilters.length > 0) {
    //   queryFiltersString = this._getWhereQueryFilter(this.whereFilters);
    //   if (queryFiltersString === undefined) {
    //     throw 'Invalid query filters ' + JSON.stringify(this.whereFilters);
    //   }
      url += '$filter=' + this.generateQueryFilterUrl() + '&';
    }
    if (this.orderBy && this.orderBy.length > 0) {
      url += '$orderby=';
      for (i in this.orderBy) {
        value = this.orderBy[i].value; //0, 1, or 2
        propertyName = this.orderBy[i].propName;
        if (propertyName) {
          switch (value) {
            case 0: {
              // Do not order by this propertyId.
              break;
            }
            case 1: {
              // Sort in asc order.
              sortingOptions.push(propertyName);
              break;
            }
            case 2: {
              // Sort in desc order.
              sortingOptions.push(propertyName + ' desc');
              break;
            }
          }
        }
      }
      // Separate the elements with a comma ',' and add the '&' at the end.
      url += sortingOptions.join() + '&';
    }
    // Remove the & at the end.
    lastUrlCharIndex = url.length - 1;
    if (url[lastUrlCharIndex] === '&') {
      url = url.substring(0, lastUrlCharIndex);
    }
    return url;
  };

  /**
   * Return the filter portion of the OData query URL.
   * @method generateQueryFilterUrl
   * @return {String} The filter URL.
   */
  OData.QueryBuilder.prototype.generateQueryFilterUrl = function() {
    var ret = '';
    if (this.whereFilters && this.whereFilters.length > 0) {
      var queryFiltersString = this._getWhereQueryFilter(this.whereFilters);
      if (queryFiltersString === undefined) {
        throw 'Invalid query filters ' + JSON.stringify(this.whereFilters);
      }
      ret = queryFiltersString;
    }
    return ret;
  };


  /**
   * Clear the filter list for the OData final query url.
   * @method removeAllWhereFilters
   */
  OData.QueryBuilder.prototype.removeAllWhereFilters = function() {
    this.whereFilters = [];
  };

  /**
   * Add a filter to the list of filter that are used to build a query string.
   * If you call this method multiple times, each filter is AND'ed together.
   * If propNames or propValues is an array of values, the resulting query
   * string is OR'ed together for that specific filter.
   * @method addWhereFilter
   * @example
   *    addWhereFilter(OData.STRING, 'FNAME', OData.EQUALS, 'bob')
   *    addWhereFilter(OData.STRING, 'LNAME', OData.EQUALS, 'smith')
   * getODataQueryUrl() would return "/api/user/?$filter=FNAME eq 'bob' and LNAME eq 'smith'"
   * @example
   *    addWhereFilter(OData.STRING, ['FNAME','LNAME'], OData.EQUALS, ['bob','smith'])
   * getODataQueryUrl() would return "/api/user/?$filter=FNAME eq 'bob' or LNAME eq 'smith'"
   * @example
   *    addWhereFilter(OData.STRING, ['FNAME','LNAME'], OData.EQUALS, 'bob')
   * getODataQueryUrl() would return "/api/user/?$filter=FNAME eq 'bob' or LNAME eq 'bob'"
   * @example
   *    addWhereFilter(OData.STRING, 'FNAME', OData.EQUALS, ['bob','sam'])
   * getODataQueryUrl() would return "/api/user/?$filter=FNAME eq 'bob' or FNAME eq 'sam'"
   * @param id {String} Unique id for a filter.
   * @param filterType {String} The property filter name(STRING, TIME, DECIMAL, etc).
   * @param propNames {Array|String} A list of property names.
   * @param filterOperator {String} The type of filter(EQUALS, CONTAINS, etc).
   * @param propValues {Array|String} The value for the filter.
   */
  OData.QueryBuilder.prototype.addWhereFilter = function(id, filterType, propNames, filterOperator, propValues) {
    var filter = {
      id: id,
      filterType: filterType,
      propNames: propNames,
      filterOperator: filterOperator,
      propValues: propValues
    };
    // Check if element already exist.
    for (var i = this.whereFilters.length - 1; i >= 0; i--) {
      if (this.whereFilters[i].id === id) {
        // Update.
        this.whereFilters[i] = filter;
        return;
      }
    }
    // Element not found: add a new one.
    this.whereFilters.push(filter);
  };

  /**
   * Delete a specific filter in the filter list.
   * @method removeWhereFilter
   * @param id {String} The id of the filter to remove.
   */
  OData.QueryBuilder.prototype.removeWhereFilter = function(id) {
    for (var i = this.whereFilters.length - 1; i >= 0; i--) {
      // Only doing double equals here because the id could be of type string or number.
      if (this.whereFilters[i].id == id) {
        this.whereFilters.splice(i, 1);
        break;
      }
    }
  };

  /**
   * Return the where filters formatted for the final OData query url.
   * @private
   * @method _getWhereQueryFilter
   * @param whereFilters {Array} A list of all the query filters.
   * @return {Array} The where filters formatted for the final OData query url.
   */
  OData.QueryBuilder.prototype._getWhereQueryFilter = function(whereFilters) {
    var i,
        l,
        filter,
        result = '';
    for (i = 0, l = whereFilters.length; i < l; i++) {
      filter = whereFilters[i];
      result += this.filterOptions.getFilterHandler(filter.filterType).getWhereQuery(
        filter.propNames, filter.filterOperator, filter.propValues);
      if (i < l - 1) {
        result += ' and ';
      }
    }
    return result;
  };

  return OData;
});