diff --git a/lib/retrieve.js b/lib/retrieve.js index 701b50c..3d2583e 100644 --- a/lib/retrieve.js +++ b/lib/retrieve.js @@ -97,7 +97,8 @@ exports.findAndLoad = function findAndLoad(searches, callback) { * Finds ids of objects by search arguments */ exports.find = function find(searches, callback) { - var self = this, sets = [], zsetKeys = [], s, prop, + var self = this, sets = [], zsetKeys = [], + regexp_uniques=[], regexp_sets=[], reKeys={sets:[], uniques:[]}, s, prop, returnFunction = function (err, values) { var found = []; Nohm.logError(err); @@ -112,10 +113,42 @@ exports.find = function find(searches, callback) { } else if (values === null) { found = []; } - convertIdsToInt(found, function (ids) { + convertIdsToInt.call(self, found, function (ids) { callback.call(self, err, ids); }); }, + regExpSearch = function(callback){ + var regExp = function(arrayKeys, fnGetKey, callback){ + if (arrayKeys.length === 0) callback(null, []) + else + { + // let's apply keys on every arrayKeys, then for each keys result we apply + // the specific data-type command to retrieve the ids + async.map(arrayKeys, self.getClient()['keys'].bind(self.getClient()), function(err, listKeys){ + if (err || listKeys.length === 0) callback(err, []) + else async.map(listKeys[0], self.getClient()[fnGetKey].bind(self.getClient()), callback) + }) + } + } + regExp(reKeys.sets, 'smembers', function(err, setResult){ + if (err) callback(err) + else + { + // smembers return an array of array, so we concat *all* of them + if (setResult.length) setResult = [].concat.apply([], setResult) + regexp_sets = regexp_sets.concat(setResult) + // get return value, so we just concat the result + regExp(reKeys.uniques, 'get', function(err, uniqueResult){ + if (err) callback(err) + else + { + regexp_uniques = regexp_uniques.concat(uniqueResult) + callback(null) + } + }) + } + }) + }, getSets = function (callback) { self.getClient().sinter(sets, callback); }, @@ -140,7 +173,10 @@ exports.find = function find(searches, callback) { return converted; } - options = zSet.options; + if (Object.prototype.toString.call(zSet.options) != '[object Object]') + options = {} + else + options = zSet.options; options.min = getRedisZSetArg(options.min, parseFloat, "-inf"); options.max = getRedisZSetArg(options.max, parseFloat, "+inf"); @@ -162,6 +198,7 @@ exports.find = function find(searches, callback) { } else if ( options.endpoints.length > 2) { return returnFunction('Invalid search parameters: endpoints expression is invalid.'); } + endpoints = [ (options.endpoints[0] === '(' ? '(' : ''), (options.endpoints[1] === ')' ? '(' : '') @@ -187,56 +224,114 @@ exports.find = function find(searches, callback) { }); }; - if (typeof searches === 'function') { callback = searches; searches = {}; } + else if (searches.regexp) + { + searches = {} + } + for (s in searches) { if (searches.hasOwnProperty(s) && this.properties.hasOwnProperty(s)) { prop = this.properties[s]; - if (prop.unique) { - if (prop.type === 'string') { - if ( ! searches[s].toLowerCase) { - return returnFunction('Invalid search parameters: Searching for a unique (type "string") with a non-string value is not supported.'); - } - searches[s] = searches[s].toLowerCase() + if (typeof searches[s] == 'object' && searches[s].regexp) + { + if (typeof searches[s].regexp == 'string' && searches[s].regexp.length) + { + if (prop.unique) + reKeys.uniques.push(Nohm.prefix.unique + self.modelName + ':' + s + ':' + searches[s].regexp); + else if (prop.index) + reKeys.sets.push(Nohm.prefix.index + self.modelName + ':' + s + ':' + searches[s].regexp); } - var key = Nohm.prefix.unique+self.modelName+':'+s+':'+searches[s]; - return this.getClient().get([key], returnFunction); + else + return returnFunction("Invalid regexp search parameters: Regular expression searching must be object {pattern:\'regexp\'}, with 'pattern' the key, and 'regexp' the regular expression."); } - var isNum = ! isNaN(parseInt(searches[s], 10)); - if (prop.index && ( ! prop.__numericIndex || isNum) ) { - sets.push(Nohm.prefix.index + self.modelName + ':' + s + ':' + searches[s]); - } else if (prop.__numericIndex) { - zsetKeys.push({ - key: Nohm.prefix.scoredindex + self.modelName + ':' + s, - options: searches[s] - }); + else + { + if (prop.unique) { + if (prop.type === 'string') { + if ( ! searches[s].toLowerCase) { + return returnFunction('Invalid search parameters: Searching for a unique (type "string") with a non-string value is not supported.'); + } + else searches[s] = searches[s].toLowerCase() + } + var key = Nohm.prefix.unique+self.modelName+':'+s+':'+searches[s]; + return this.getClient().get([key], returnFunction); + } + var isNum = ! isNaN(parseInt(searches[s], 10)); + if (prop.index && ( ! prop.__numericIndex || isNum) ) { + sets.push(Nohm.prefix.index + self.modelName + ':' + s + ':' + searches[s]); + } else if (prop.__numericIndex) { + zsetKeys.push({ + key: Nohm.prefix.scoredindex + self.modelName + ':' + s, + options: searches[s] + }); + } } } } - if (sets.length === 0 && zsetKeys.length === 0) { - if (JSON.stringify(searches) != '{}') { - Nohm.logError("Invalid search: Index not found."); - return returnFunction(null, []); - } - // no specific searches, retrieve all ids - this.getClient().smembers(Nohm.prefix.idsets + this.modelName, returnFunction); - } else if (zsetKeys.length === 0) { - getSets(returnFunction); - } else if (sets.length === 0) { - getZSets(returnFunction); - } else { - getSets(function (err, setids) { - getZSets(function (err2, zsetids) { - if (err2) { - err = [err, err2]; + // we shouldnt need 'this' here, so no need to bind it + return regExpSearch(function(err){ + if (err) returnFunction(err) + else + { + if (sets.length === 0 && zsetKeys.length === 0 && regexp_uniques.length === 0 + && regexp_sets.length === 0) { + if (JSON.stringify(searches) != '{}') { + Nohm.logError("Invalid search: Index not found."); + return returnFunction(null, []); } - returnFunction(err, h.idIntersection(setids, zsetids).sort()); - }); - }); - } + // no specific searches, retrieve all ids + self.getClient().smembers(Nohm.prefix.idsets + self.modelName, returnFunction); + } + else { + var searchScope = { + regexp_uniques:{ + fn: function(cb){cb(null, regexp_uniques)}, + value: regexp_uniques, + }, + regexp_sets:{ + fn: function(cb){cb(null, regexp_sets)}, + value: regexp_sets, + }, + standard_index:{ + fn:getSets, + value: sets, + }, + standard_scoredIndex:{ + fn:getZSets, + value: zsetKeys, + } + } + /* + To make an OR operator, you just need to set null to [] and change + "if (!memo)" to "if (!memo.length)" + */ + async.reduce(Object.keys(searchScope), null, function(memo, item, callback){ + if (searchScope[item].value.length) + { + searchScope[item].fn(function(err, ids){ + if (!memo) memo = ids + else memo = h.idIntersection(memo, ids) + callback(null, memo) + }) + } + else callback(null, memo) + }, function(err, result){ + /* + In the initial find function, there is a sort when sets/zsetKeys are not empty + I don't know why, but we do it to avoid some tests fails because + they expect specific order + */ + if ((sets.length && zsetKeys.length)||regexp_sets.length||regexp_uniques.length) + result = result.sort() + returnFunction(err, result); + }) + } + } + }) }; exports.sort = function (options, ids) { diff --git a/test/findTests.js b/test/findTests.js index 9f07cef..0c389eb 100644 --- a/test/findTests.js +++ b/test/findTests.js @@ -1155,7 +1155,7 @@ loadArray: function (t) { endpoints: '(' } }, function(err, ids) { - t.ok(!err, 'Unexpected redis error in custom query'); + t.ok(!err, 'Unexpected redis error in custom query :' + err); t.same([7, 6, 5, 4], ids, 'Defining an endpoint failed.'); UserFindMockup.find({ number: { @@ -1236,7 +1236,161 @@ loadArray: function (t) { t.same(ids, [2, 3, 8], 'The found ids were incorrect.'); t.done(); }); - } + }, + + "star '*pattern*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:'*numeric*' + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [], 'This shouldnt find any ids.'); + t.done(); + }); + }, + + "regexp - field with star '*pattern*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'*numeric*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1,2,3], 'This should find three ids.'); + t.done(); + }); + }, + + "regexp - field with star 'pattern*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'unique*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [4], 'This should return one id.'); + t.done(); + }); + }, + "regexp - field with star '[p][a][t][t][e][r][n]*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'[u][n][i][q][u][e]*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [4], 'This should return one id.'); + t.done(); + }); + }, + + "regexp - field with star '*[_]*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'*[_]*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [7,8], 'This should return two ids.'); + t.done(); + }); + }, + + "regexp - field with star '*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1,2,3,4,5,6,7,8], 'This should find all ids.'); + t.done(); + }); + }, + + "regexp - unique field with star '*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + email:{ + regexp:'*' + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1,2,3,4,5,6,7,8], 'This should find all ids.'); + t.done(); + }); + }, + + "regexp - unique field with regexp field star '*'": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + name:{ + regexp:'*' + }, + email:'numericindextest@hurgel.de', + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1], 'This should find one id.'); + t.done(); + }); + }, + + "regexp - field with star '*@hu*el.de' + numeric with min without limit": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + email:{ + regexp:'*@hu*el.de' + }, + number: { + min: 2, + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1, 2, 3, 8], 'The found ids were incorrect.'); + t.done(); + }); + }, + + "regexp - field with star '*' + numeric with min without limit": function (t) { + var findUser = new UserFindMockup(); + t.expect(2); + + findUser.find({ + email:{ + regexp:'*' + }, + number: { + min: 0, + } + }, function(err, ids) { + t.ok(!err, 'Unexpected redis error in custom query'); + t.same(ids, [1, 2, 3, 4, 5, 6, 7, 8], 'The found ids were incorrect.'); + t.done(); + }); + } };