UNPKG

prepared-query-options

Version:

JS Library for managing OData query parameter building and parsing

626 lines (553 loc) 21.7 kB
/* * Predicate * version: 1.2.2 * author: David Hamilton * license: https://github.com/nova706/PreparedQueryOptions/blob/master/LICENSE.txt (MIT) * https://github.com/nova706/PreparedQueryOptions * */ (function () { /** * A predicate is used for the $filter operator in a query. Predicates can be joined to query * using a group of filters with the 'and' operator. * * This is a helper class for the PreparedQueryOptions class to assist in building complex * filter clauses. * * @class Predicate * @constructor * @param {String} [property] The property to filter by. * @param {Function} [getValue] A function that returns the predicate string. */ function Predicate(property, getValue) { this.property = property; this.getValue = getValue; return this; } /** * Joins a provided set of predicates using the group operator and returns a new Predicate * * @method join * @param {Predicate[]} predicates Array of predicates to join. * @param {String} [groupOperator] The operator for the filter set ('and' 'or'). * @return {Predicate} Predicate object. */ Predicate.join = function (predicates, groupOperator) { if (predicates instanceof Array && predicates.length > 0) { return new Predicate().join(predicates, groupOperator); } return null; }; /** * Sets the predicate's property * * @method setProperty * @param {String} property * @return {Predicate} Predicate object. */ Predicate.prototype.setProperty = function (property) { this.property = property; return this; }; /** * Modifies an existing predicate setting the operator to 'eq' and the value to the input parameter * * @method equals * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.equals = function (value) { this.getValue = function () { return this.property + ' eq ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operator to 'ne' and the value to the input parameter * * @method notEqualTo * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.notEqualTo = function (value) { this.getValue = function () { return this.property + ' ne ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operator to 'gt' and the value to the input parameter * * @method greaterThan * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.greaterThan = function (value) { this.getValue = function () { return this.property + ' gt ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operator to 'ge' and the value to the input parameter * * @method greaterThanOrEqualTo * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.greaterThanOrEqualTo = function (value) { this.getValue = function () { return this.property + ' ge ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operator to 'lt' and the value to the input parameter * * @method lessThan * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.lessThan = function (value) { this.getValue = function () { return this.property + ' lt ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operator to 'le' and the value to the input parameter * * @method lessThanOrEqualTo * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.lessThanOrEqualTo = function (value) { this.getValue = function () { return this.property + ' le ' + escapeValue(value); }; return this; }; /** * Modifies an existing predicate setting the operation to contains and the value to the input parameter * * @method contains * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.contains = function (value) { this.getValue = function () { return 'contains(' + this.property + ', ' + escapeValue(value) + ')'; }; return this; }; /** * Modifies an existing predicate setting the operation to startswith and the value to the input parameter * * @method startsWith * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.startsWith = function (value) { this.getValue = function () { return 'startswith(' + this.property + ', ' + escapeValue(value) + ')'; }; return this; }; /** * Modifies an existing predicate setting the operation to endswith and the value to the input parameter * * @method startsWith * @param {String|Number|Boolean} (value) The value to match. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.endsWith = function (value) { this.getValue = function () { return 'endswith(' + this.property + ', ' + escapeValue(value) + ')'; }; return this; }; /** * Joins an existing predicate with additional predicates using the group operator * * @method join * @param {Predicate|Predicate[]} predicates A single predicate or an array of predicates to join to the existing one. * @param {String} [groupOperator] The operator for the filter set ('and' 'or'). * @return {Predicate} Used for chaining function calls */ Predicate.prototype.join = function (predicates, groupOperator) { var initialPredicate; if (this.property && typeof this.getValue === 'function') { initialPredicate = new Predicate(this.property, this.getValue); } var newPredicates = []; if (predicates instanceof Predicate) { newPredicates.push(predicates); } else if (predicates instanceof Array && predicates.length > 0) { var i; for (i = 0; i < predicates.length; i++) { if (predicates[i]) { newPredicates.push(predicates[i]); } } } if (newPredicates.length > 0) { delete this.getValue; delete this.property; this.joinedPredicates = (this.joinedPredicates) ? this.joinedPredicates.concat(newPredicates) : newPredicates; if (groupOperator || !this.groupOperator) { this.groupOperator = (groupOperator && groupOperator.toLowerCase() === Predicate.GROUP_OPERATOR.OR) ? Predicate.GROUP_OPERATOR.OR : Predicate.GROUP_OPERATOR.AND; } if (initialPredicate) { this.joinedPredicates.unshift(initialPredicate); } } return this; }; /** * Joins an existing predicate with additional predicates using the 'and' group operator * * @method and * @param {Predicate|Predicate[]} predicates A single predicate or an array of predicates to join to the existing one. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.and = function (predicates) { return this.join(predicates, Predicate.GROUP_OPERATOR.AND); }; /** * Joins an existing predicate with additional predicates using the 'or' group operator * * @method or * @param {Predicate|Predicate[]} predicates A single predicate or an array of predicates to join to the existing one. * @return {Predicate} Used for chaining function calls */ Predicate.prototype.or = function (predicates) { return this.join(predicates, Predicate.GROUP_OPERATOR.OR); }; /** * Evaluate an object to see if it matches the predicate filter conditions. * * @method test * @param {Object} object The object to test against the predicate. * @return {Boolean} True if the object matches the filter conditions. */ Predicate.prototype.test = function (object) { return testPredicate(this, object); }; /** * Builds and returns a URL parameter string based on the predicate. * * @method toString * @param {Boolean} [nested = false] Used for building the nested group during recursion * @returns {String} */ Predicate.prototype.toString = function (nested) { nested = (nested === true); var urlString = ''; if (this.property && typeof this.getValue === 'function') { return this.getValue(); } if (this.joinedPredicates && this.joinedPredicates.length > 0) { var i; var predicate; var predicateString; for (i = 0; i < this.joinedPredicates.length; i++) { predicate = this.joinedPredicates[i]; predicateString = predicate.toString(true); urlString += (i > 0) ? ' ' + this.groupOperator + ' ' + predicateString : predicateString; } } return nested ? '(' + urlString + ')' : urlString; }; /** * Utility method for preparedQueryOptions class * @returns {Boolean} True * @private */ Predicate.prototype._isPredicate = function () { return true; }; /** * Creates a predicate structure from a string * * @method fromString * @param {String} predicateString * @return {Predicate|null} null if the predicate could not be built from the string */ Predicate.fromString = function (predicateString) { if (typeof predicateString !== "string") { return null; } // Extract all the filters out of the predicate string var conditionMatcher = new RegExp("(contains\\(.+?\\)|startswith\\(.+?\\)|endswith\\(.+?\\)|[\\w\\.]+?\\s(?:eq|ne|gt|ge|lt|le)\\s(?:\\w+|\\'.+?\\'))", "g"); var filters = predicateString.match(conditionMatcher); if (!filters) { return null; } // Convert each filter into a predicate var i; for (i = 0; i < filters.length; i++) { filters[i] = getPredicateFromSegment(filters[i]); if (filters[i] === null) { return null; } } if (filters.length === 0) { return null; } // Remove all predicates from string i = 0; predicateString = predicateString.replace(conditionMatcher, function () { return i++; }); if (filters.length === 1) { if (predicateString.replace(/[0-9]|\s|and|or/gi, "") !== "") { return null; } return filters[0]; } var closeParenthesisIndex; var openParenthesisIndex; var groupString; var filterIndexes; var groupPredicate = null; var groupFilters; var operator; var testNextLevel = true; while (testNextLevel) { closeParenthesisIndex = predicateString.indexOf(')'); if (closeParenthesisIndex !== -1) { openParenthesisIndex = predicateString.lastIndexOf('(', closeParenthesisIndex); groupString = predicateString.substring(openParenthesisIndex + 1, closeParenthesisIndex); predicateString = predicateString.substring(0, openParenthesisIndex) + filters.length + predicateString.substring(closeParenthesisIndex + 1); } else { groupString = predicateString; testNextLevel = false; } // If the group contains invalid characters then return null as an invalid predicate string. if (groupString.replace(/[0-9]|\s|and|or/gi, "") !== "") { return null; } // If the group uses both 'and' and 'or' then return null as an invalid predicate string. if (groupString.toLowerCase().indexOf(Predicate.GROUP_OPERATOR.AND) >= 0 && groupString.toLowerCase().indexOf(Predicate.GROUP_OPERATOR.OR) >= 0) { return null; } filterIndexes = groupString.match(/[0-9]+/g); groupFilters = []; for (i = 0; i < filterIndexes.length; i++) { groupFilters.push(filters[Number(filterIndexes[i])]); } operator = groupString.toLowerCase().indexOf(Predicate.GROUP_OPERATOR.OR) >= 0 ? Predicate.GROUP_OPERATOR.OR : Predicate.GROUP_OPERATOR.AND; groupPredicate = new Predicate().join(groupFilters, operator); filters.push(groupPredicate); } return groupPredicate; }; Predicate.GROUP_OPERATOR = { 'OR': 'or', 'AND': 'and' }; /** * Takes a predicate's value and if it is a string, adds single quotes around it. * * @method escapeValue * @param {String|Boolean|Number} value * @returns {string} The string value */ var escapeValue = function (value) { return (typeof value === 'string') ? "'" + value + "'" : value.toString(); }; /** * Returns the raw value of the predicate string * * @method convertValueToType * @param {String} value * @returns {String|Boolean|Number} */ var convertValueToType = function (value) { if (value.indexOf("'") >= 0) { return value.replace(/\'/g, ''); } if (value.toLowerCase() === 'true') { return true; } if (value.toLowerCase() === 'false') { return false; } if (!isNaN(value)) { return Number(value); } return value; }; /** * Tests an object to see if the filter conditions match a given predicate. Used for recursive tests. * * @param {Predicate} predicate * @param {Object} object */ var testPredicate = function (predicate, object) { var i; if (predicate.joinedPredicates && predicate.joinedPredicates.length > 0) { var result; for (i = 0; i < predicate.joinedPredicates.length; i++) { result = testPredicate(predicate.joinedPredicates[i], object); // If the operator is 'and' and any of the filters do not match, return false. if (predicate.groupOperator === Predicate.GROUP_OPERATOR.AND && result === false) { return false; } // If the operator is 'or' and any of the filters match, return true. if (predicate.groupOperator === Predicate.GROUP_OPERATOR.OR && result === true) { return true; } } // The operator was 'and' and all of the filters matched or the operator was 'or' and none of the filters matched. return predicate.groupOperator === Predicate.GROUP_OPERATOR.AND; } if (predicate.property) { var propertyPath = predicate.property.split('.'); var objectValue = object; for (i = 0; i < propertyPath.length; i++) { if (objectValue.hasOwnProperty(propertyPath[i])) { objectValue = objectValue[propertyPath[i]]; } else { return false; } } var condition = predicate.toString(); var operator; var conditionParams; var value; if (condition.indexOf('(') >= 0) { operator = condition.substr(0, condition.indexOf('(')); var start = condition.indexOf('(') + 1; var end = condition.indexOf(')') - start; conditionParams = condition.substr(start, end); conditionParams = conditionParams.replace(/\'/g, '').split(', '); objectValue = objectValue.toString().toLowerCase(); switch (operator) { case 'startswith': value = conditionParams[1].toLowerCase(); return (objectValue.indexOf(value) === 0); case 'endswith': value = conditionParams[1].toLowerCase(); return (objectValue.indexOf(value) === objectValue.length - 1 - value.length); case 'contains': value = conditionParams[1].toLowerCase(); return (objectValue.indexOf(value) >= 0); } return false; } conditionParams = condition.split(' '); operator = conditionParams[1]; value = conditionParams.slice(2); value = value.join(' '); if (value.indexOf("'") >= 0) { // The value is a string value = value.replace(/\'/g, ''); } else if (!isNaN(value)) { // The value is a number value = Number(value); } else if (value.toLowerCase() === 'false') { // The value is a boolean value = false; } else if (value.toLowerCase() === 'true') { // The value is a boolean value = true; } var resultValue = objectValue; if (resultValue instanceof Date && !isNaN(Date.parse(value))) { value = Date.parse(value); } else if (typeof resultValue === 'string' && !isNaN(Date.parse(resultValue) && !isNaN(Date.parse(value)))) { resultValue = Date.parse(resultValue); value = Date.parse(value); } switch (operator) { case 'lt': return resultValue < value; case 'gt': return resultValue > value; case 'le': return resultValue <= value; case 'ge': return resultValue >= value; case 'ne': return resultValue != value; case 'eq': return resultValue == value; } } return false; }; /** * Creates a predicate from a single condition eg: "property eq 'value'" * * @param {String} condition * @return {Predicate} The predicate built from the condition */ var getPredicateFromSegment = function (condition) { var parenPos = condition.indexOf('('); var predicate = null; var conditionParams; var operator; var value; if (parenPos >= 0) { operator = condition.substring(0, parenPos); var start = parenPos + 1; var end = condition.indexOf(')'); conditionParams = condition.substring(start, end); conditionParams = conditionParams.split(', '); switch (operator) { case 'startswith': value = convertValueToType(conditionParams[1]); predicate = new Predicate(conditionParams[0]).startsWith(value); break; case 'endswith': value = convertValueToType(conditionParams[1]); predicate = new Predicate(conditionParams[0]).endsWith(value); break; case 'contains': value = convertValueToType(conditionParams[1]); predicate = new Predicate(conditionParams[0]).contains(value); break; } return predicate; } conditionParams = condition.split(' '); operator = conditionParams[1]; value = convertValueToType(conditionParams.slice(2).join(' ')); predicate = new Predicate(conditionParams[0]); switch (operator) { case 'eq': predicate.equals(value); break; case 'ne': predicate.notEqualTo(value); break; case 'gt': predicate.greaterThan(value); break; case 'ge': predicate.greaterThanOrEqualTo(value); break; case 'lt': predicate.lessThan(value); break; case 'le': predicate.lessThanOrEqualTo(value); break; } return predicate; }; /*globals module, define*/ if (typeof module !== 'undefined' && module.exports) { // Expose this class for node.js module.exports = Predicate; } else if (typeof define === 'function' && define.amd) { // Expose this class for requireJS define(function () { return Predicate; }); } else { // Expose this class as a global variable this.Predicate = Predicate; } }).call(this);