diff --git a/src/queries/core/complexquery.js b/src/queries/core/complexquery.js index 72bff72cf1f05c616f02f3c92a111e3a61363d81..9015739a58e1c3ff6c9ab42299ef900aab3314b4 100644 --- a/src/queries/core/complexquery.js +++ b/src/queries/core/complexquery.js @@ -48,8 +48,12 @@ inherits(ComplexQuery, Query); /** * #crossLink "Query/match:method" */ -ComplexQuery.prototype.match = function (item, wildcard_character) { - return this[this.operator](item, wildcard_character); +ComplexQuery.prototype.match = function (item) { + var operator = this.operator; + if (!(/^(?:AND|OR|NOT)$/i.test(operator))) { + operator = "AND"; + } + return this[operator.toUpperCase()](item); }; /** @@ -79,6 +83,7 @@ ComplexQuery.prototype.serialized = function () { }); return s; }; +ComplexQuery.prototype.toJSON = ComplexQuery.prototype.serialized; /** * Comparison operator, test if all sub queries match the @@ -86,13 +91,12 @@ ComplexQuery.prototype.serialized = function () { * * @method AND * @param {Object} item The item to match - * @param {String} wildcard_character The wildcard character * @return {Boolean} true if all match, false otherwise */ -ComplexQuery.prototype.AND = function (item, wildcard_character) { +ComplexQuery.prototype.AND = function (item) { var j, promises = []; for (j = 0; j < this.query_list.length; j += 1) { - promises.push(this.query_list[j].match(item, wildcard_character)); + promises.push(this.query_list[j].match(item)); } function cancel() { @@ -133,13 +137,12 @@ ComplexQuery.prototype.AND = function (item, wildcard_character) { * * @method OR * @param {Object} item The item to match - * @param {String} wildcard_character The wildcard character * @return {Boolean} true if one match, false otherwise */ -ComplexQuery.prototype.OR = function (item, wildcard_character) { +ComplexQuery.prototype.OR = function (item) { var j, promises = []; for (j = 0; j < this.query_list.length; j += 1) { - promises.push(this.query_list[j].match(item, wildcard_character)); + promises.push(this.query_list[j].match(item)); } function cancel() { @@ -180,12 +183,11 @@ ComplexQuery.prototype.OR = function (item, wildcard_character) { * * @method NOT * @param {Object} item The item to match - * @param {String} wildcard_character The wildcard character * @return {Boolean} true if one match, false otherwise */ -ComplexQuery.prototype.NOT = function (item, wildcard_character) { +ComplexQuery.prototype.NOT = function (item) { return sequence([function () { - return this.query_list[0].match(item, wildcard_character); + return this.query_list[0].match(item); }, function (answer) { return !answer; }]); diff --git a/src/queries/core/parser.par b/src/queries/core/parser.par index c235cff8e1e918e0ee1b43728065130da4144180..242bf8ed0e283f3436c86187320cb1a03f429c9e 100644 --- a/src/queries/core/parser.par +++ b/src/queries/core/parser.par @@ -62,7 +62,11 @@ var arrayExtend = function () { return newlist; }, mkSimpleQuery = function (key, value, operator) { - return {"type": "simple", "operator": "=", "key": key, "value": value}; + var object = {"type": "simple", "key": key, "value": value}; + if (operator !== undefined) { + object.operator = operator; + } + return object; }, mkNotQuery = function (query) { if (query.operator === "NOT") { diff --git a/src/queries/core/query.js b/src/queries/core/query.js index fb6eebc4862a8215b44981562bfe3584f81b8766..dee09838c826937f98be5d10f580c7626b1fc73e 100644 --- a/src/queries/core/query.js +++ b/src/queries/core/query.js @@ -56,7 +56,6 @@ function Query() { * @method exec * @param {Array} item_list The list of object * @param {Object} [option] Some operation option - * @param {String} [option.wildcard_character="%"] The wildcard character * @param {Array} [option.select_list] A object keys to retrieve * @param {Array} [option.sort_on] Couples of object keys and "ascending" * or "descending" @@ -75,14 +74,11 @@ Query.prototype.exec = function (item_list, option) { throw new TypeError("Query().exec(): " + "Optional argument 2 is not of type 'object'"); } - if (option.wildcard_character === undefined) { - option.wildcard_character = '%'; - } for (i = 0; i < item_list.length; i += 1) { if (!item_list[i]) { promises.push(RSVP.resolve(false)); } else { - promises.push(this.match(item_list[i], option.wildcard_character)); + promises.push(this.match(item_list[i])); } } return sequence([function () { @@ -113,7 +109,6 @@ Query.prototype.exec = function (item_list, option) { * * @method match * @param {Object} item The object to test - * @param {String} wildcard_character The wildcard character to use * @return {Boolean} true if match, false otherwise */ Query.prototype.match = function () { diff --git a/src/queries/core/serializer.js b/src/queries/core/serializer.js index c4d62967b55dd9337b7ec2a6401b855b7fa14931..56fa53d691633ba47acea0e3dd9ffdcc370268ab 100644 --- a/src/queries/core/serializer.js +++ b/src/queries/core/serializer.js @@ -1,5 +1,5 @@ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ -/*global _export: true, to_export: true */ +/*global _export: true */ function objectToSearchText(query) { var str_list = []; @@ -15,7 +15,7 @@ function objectToSearchText(query) { } if (query.type === "simple") { return (query.key ? query.key + ": " : "") + - (query.operator || "=") + ' "' + query.value + '"'; + (query.operator || "") + ' "' + query.value + '"'; } throw new TypeError("This object is not a query"); } diff --git a/src/queries/core/simplequery.js b/src/queries/core/simplequery.js index f354d9e143202e6f84fa6be56ee69e8f570fb9ee..e3283d300c34db07ecf9b45d41c195493c50c203 100644 --- a/src/queries/core/simplequery.js +++ b/src/queries/core/simplequery.js @@ -1,6 +1,6 @@ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*global Query: true, inherits: true, query_class_dict: true, _export: true, - convertStringToRegExp, RSVP */ + searchTextToRegExp, RSVP */ var checkKeySchema = function (key_schema) { var prop; @@ -54,10 +54,9 @@ function SimpleQuery(spec, key_schema) { * * @attribute operator * @type String - * @default "=" * @optional */ - this.operator = spec.operator || "="; + this.operator = spec.operator; /** * Key of the object which refers to the value to compare @@ -105,15 +104,28 @@ var checkKey = function (key) { /** * #crossLink "Query/match:method" */ -SimpleQuery.prototype.match = function (item, wildcard_character) { +SimpleQuery.prototype.match = function (item) { var object_value = null, equal_match = null, cast_to = null, matchMethod = null, + operator = this.operator, value = null, key = this.key; - matchMethod = this[this.operator]; + /*jslint regexp: true */ + if (!(/^(?:!?=|<=?|>=?)$/i.test(operator))) { + // `operator` is not correct, we have to change it to "like" or "=" + if (/%/.test(this.value)) { + // `value` contains a non escaped `%` + operator = "like"; + } else { + // `value` does not contain non escaped `%` + operator = "="; + } + } + + matchMethod = this[operator]; if (this._key_schema.key_set && this._key_schema.key_set[key] !== undefined) { key = this._key_schema.key_set[key]; @@ -133,7 +145,8 @@ SimpleQuery.prototype.match = function (item, wildcard_character) { // equal_match overrides the default '=' operator if (equal_match !== undefined) { - matchMethod = (this.operator === '=') ? equal_match : matchMethod; + matchMethod = (operator === "=" || operator === "like" ? + equal_match : matchMethod); } value = this.value; @@ -155,28 +168,32 @@ SimpleQuery.prototype.match = function (item, wildcard_character) { if (object_value === undefined || value === undefined) { return RSVP.resolve(false); } - return matchMethod(object_value, value, wildcard_character); + return matchMethod(object_value, value); }; /** * #crossLink "Query/toString:method" */ SimpleQuery.prototype.toString = function () { - return (this.key ? this.key + ": " : "") + (this.operator || "=") + ' "' + - this.value + '"'; + return (this.key ? this.key + ":" : "") + + (this.operator ? " " + this.operator : "") + ' "' + this.value + '"'; }; /** * #crossLink "Query/serialized:method" */ SimpleQuery.prototype.serialized = function () { - return { + var object = { "type": "simple", - "operator": this.operator, "key": this.key, "value": this.value }; + if (this.operator !== undefined) { + object.operator = this.operator; + } + return object; }; +SimpleQuery.prototype.toJSON = SimpleQuery.prototype.serialized; /** * Comparison operator, test if this query value matches the item value @@ -184,11 +201,40 @@ SimpleQuery.prototype.serialized = function () { * @method = * @param {String} object_value The value to compare * @param {String} comparison_value The comparison value - * @param {String} wildcard_character The wildcard_character * @return {Boolean} true if match, false otherwise */ -SimpleQuery.prototype["="] = function (object_value, comparison_value, - wildcard_character) { +SimpleQuery.prototype["="] = function (object_value, comparison_value) { + var value, i; + if (!Array.isArray(object_value)) { + object_value = [object_value]; + } + for (i = 0; i < object_value.length; i += 1) { + value = object_value[i]; + if (typeof value === 'object' && value.hasOwnProperty('content')) { + value = value.content; + } + if (typeof value.cmp === "function") { + return RSVP.resolve(value.cmp(comparison_value) === 0); + } + if ( + searchTextToRegExp(comparison_value.toString(), false). + test(value.toString()) + ) { + return RSVP.resolve(true); + } + } + return RSVP.resolve(false); +}; + +/** + * Comparison operator, test if this query value matches the item value + * + * @method like + * @param {String} object_value The value to compare + * @param {String} comparison_value The comparison value + * @return {Boolean} true if match, false otherwise + */ +SimpleQuery.prototype.like = function (object_value, comparison_value) { var value, i; if (!Array.isArray(object_value)) { object_value = [object_value]; @@ -198,15 +244,11 @@ SimpleQuery.prototype["="] = function (object_value, comparison_value, if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { - return RSVP.resolve(value.cmp(comparison_value, - wildcard_character) === 0); + if (typeof value.cmp === "function") { + return RSVP.resolve(value.cmp(comparison_value) === 0); } if ( - convertStringToRegExp( - comparison_value.toString(), - wildcard_character - ).test(value.toString()) + searchTextToRegExp(comparison_value.toString()).test(value.toString()) ) { return RSVP.resolve(true); } @@ -220,11 +262,9 @@ SimpleQuery.prototype["="] = function (object_value, comparison_value, * @method != * @param {String} object_value The value to compare * @param {String} comparison_value The comparison value - * @param {String} wildcard_character The wildcard_character * @return {Boolean} true if not match, false otherwise */ -SimpleQuery.prototype["!="] = function (object_value, comparison_value, - wildcard_character) { +SimpleQuery.prototype["!="] = function (object_value, comparison_value) { var value, i; if (!Array.isArray(object_value)) { object_value = [object_value]; @@ -234,15 +274,12 @@ SimpleQuery.prototype["!="] = function (object_value, comparison_value, if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { - return RSVP.resolve(value.cmp(comparison_value, - wildcard_character) !== 0); + if (typeof value.cmp === "function") { + return RSVP.resolve(value.cmp(comparison_value) !== 0); } if ( - convertStringToRegExp( - comparison_value.toString(), - wildcard_character - ).test(value.toString()) + searchTextToRegExp(comparison_value.toString(), false). + test(value.toString()) ) { return RSVP.resolve(false); } @@ -267,7 +304,7 @@ SimpleQuery.prototype["<"] = function (object_value, comparison_value) { if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { + if (typeof value.cmp === "function") { return RSVP.resolve(value.cmp(comparison_value) < 0); } return RSVP.resolve(value < comparison_value); @@ -291,7 +328,7 @@ SimpleQuery.prototype["<="] = function (object_value, comparison_value) { if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { + if (typeof value.cmp === "function") { return RSVP.resolve(value.cmp(comparison_value) <= 0); } return RSVP.resolve(value <= comparison_value); @@ -315,7 +352,7 @@ SimpleQuery.prototype[">"] = function (object_value, comparison_value) { if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { + if (typeof value.cmp === "function") { return RSVP.resolve(value.cmp(comparison_value) > 0); } return RSVP.resolve(value > comparison_value); @@ -339,7 +376,7 @@ SimpleQuery.prototype[">="] = function (object_value, comparison_value) { if (typeof value === 'object' && value.hasOwnProperty('content')) { value = value.content; } - if (value.cmp !== undefined) { + if (typeof value.cmp === "function") { return RSVP.resolve(value.cmp(comparison_value) >= 0); } return RSVP.resolve(value >= comparison_value); diff --git a/src/queries/core/tool.js b/src/queries/core/tool.js index 06b5f191c0c309ec09f625b993491c0900b068fa..c0985589ad3e9daf49074c3a0f4dad0318dfca6f 100644 --- a/src/queries/core/tool.js +++ b/src/queries/core/tool.js @@ -264,29 +264,27 @@ _export('limit', limit); * Convert a search text to a regexp. * * @param {String} string The string to convert - * @param {String} [wildcard_character=undefined] The wildcard chararter + * @param {Boolean} [use_wildcard_character=true] Use wildcard "%" and "_" * @return {RegExp} The search text regexp */ -function convertStringToRegExp(string, wildcard_character) { +function searchTextToRegExp(string, use_wildcard_characters) { if (typeof string !== 'string') { - throw new TypeError("complex_queries.convertStringToRegExp(): " + + throw new TypeError("complex_queries.searchTextToRegExp(): " + "Argument 1 is not of type 'string'"); } - if (wildcard_character === undefined || - wildcard_character === null || wildcard_character === '') { + if (use_wildcard_characters === false) { return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$"); } - if (typeof wildcard_character !== 'string' || wildcard_character.length > 1) { - throw new TypeError("complex_queries.convertStringToRegExp(): " + - "Optional argument 2 must be a string of length <= 1"); - } return new RegExp("^" + stringEscapeRegexpCharacters(string).replace( - new RegExp(stringEscapeRegexpCharacters(wildcard_character), 'g'), - '.*' + /%/g, + ".*" + ).replace( + /_/g, + "." ) + "$"); } -_export('convertStringToRegExp', convertStringToRegExp); +_export("searchTextToRegExp", searchTextToRegExp); /** * sequence(thens): Promise diff --git a/test/queries/key.tests.js b/test/queries/key.tests.js index 9c9c9b65473cc3472c5ed1fc09b43a2d9753f61e..bf4bb57fa276d1c4cd9ebee0f9786041f7eda1c7 100644 --- a/test/queries/key.tests.js +++ b/test/queries/key.tests.js @@ -22,7 +22,6 @@ }; test('Simple Key with read_from', function () { - /*jslint unparam: true*/ var docList = function () { return [ {'identifier': 'a'}, @@ -35,12 +34,11 @@ }, case_insensitive_identifier: { read_from: 'identifier', - equal_match: function (object_value, value, wildcard_character) { + equal_match: function (object_value, value) { return (object_value.toLowerCase() === value.toLowerCase()); } } }, promise = []; - /*jslint unparam: false*/ stop(); diff --git a/test/queries/tests.js b/test/queries/tests.js index 556a99347d8098e9cab59827f99bf0f07a6523e2..fc991aeab5b83410e9985d64973bcc8a4d790e11 100644 --- a/test/queries/tests.js +++ b/test/queries/tests.js @@ -105,44 +105,66 @@ var doc_list = [ {"identifier": "a"}, {"identifier": "a%"}, + {"identifier": "a\\%"}, {"identifier": ["ab", "b"]} ]; stop(); - complex_queries.QueryFactory.create('identifier: "a%"').exec(doc_list, { - // "wildcard_character": "%" // default - }).then(function (doc_list) { + complex_queries.QueryFactory.create('identifier: "a%"').exec( + doc_list + ).then(function (doc_list) { deepEqual(doc_list, [ {"identifier": "a"}, {"identifier": "a%"}, + {"identifier": "a\\%"}, {"identifier": ["ab", "b"]} ], 'All documents should be kept'); doc_list = [ {"identifier": "a"}, {"identifier": "a%"}, + {"identifier": "a\\%"}, {"identifier": ["ab", "b"]} ]; - return complex_queries.QueryFactory.create('identifier: "a%"'). - exec(doc_list, {"wildcard_character": null}); + return complex_queries.QueryFactory.create('identifier: "a\\%"'). + exec(doc_list); }).then(function (doc_list) { deepEqual(doc_list, [ - {"identifier": "a%"} - ], 'Document "a%" should be kept'); + {"identifier": "a\\%"} + ], 'Only third document should be kept'); + // yes.. it's weird but ERP5 acts like that. + // `\` (or "\\") is taken literaly (= /\\/) doc_list = [ {"identifier": "a"}, {"identifier": "a%"}, + {"identifier": "a\\%"}, {"identifier": ["ab", "b"]} ]; - return complex_queries.QueryFactory.create('identifier: "b"'). - exec(doc_list, {"wildcard_character": "b"}); + + return complex_queries.QueryFactory.create('identifier: "__"'). + exec(doc_list); }).then(function (doc_list) { deepEqual(doc_list, [ + ], 'Should keep nothing'); + + doc_list = [ {"identifier": "a"}, {"identifier": "a%"}, + {"identifier": "a\\%"}, {"identifier": ["ab", "b"]} - ], 'All documents should be kept'); + ]; + + return complex_queries.QueryFactory.create('identifier: "__%"'). + exec(doc_list); + }).then(function (doc_list) { + deepEqual(doc_list, [ + {"identifier": "a%"}, + {"identifier": "a\\%"}, + {"identifier": ["ab", "b"]} + ], 'First should not be kept'); + // yes.. it's weird but ERP5 acts like that. + // `_` is not considered as wildcard (= /./) }).always(start); }); @@ -164,4 +186,46 @@ }).always(start); }); + test("JSON query", function () { + var jsoned = complex_queries.QueryFactory.create( + "NOT(a:=b OR c:% AND d:<2)" + ).toJSON(); + deepEqual( + jsoned, + { + "type": "complex", + "operator": "NOT", + "query_list": [{ + "type": "complex", + "operator": "OR", + "query_list": [{ + "key": "a", + "operator": "=", + "type": "simple", + "value": "b" + }, { + "type": "complex", + "operator": "AND", + "query_list": [{ + "key": "c", + "type": "simple", + "value": "%" + }, { + "key": "d", + "operator": "<", + "type": "simple", + "value": "2" + }] + }] + }] + }, + "\"NOT(a:=b OR c:% AND d:<2)\".toJSON()" + ); + deepEqual( + complex_queries.parseStringToObject("NOT(a:=b OR c:% AND d:<2)"), + jsoned, + "parseStringToObject(\"NOT(a:=b OR c:% AND d:<2)\");" + ); + }); + }));