diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 94ca3d9f..2b18e0c4 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -350,9 +350,8 @@ Memory.prototype.all = function all(model, filter, options, callback) { } // do we need some filtration? - if (filter.where) { - nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes; - } + if (filter.where && nodes) + nodes = nodes.filter(applyFilter(filter)); // field selection if (filter.fields) { @@ -401,7 +400,7 @@ function applyFilter(filter) { var keys = Object.keys(where); return function (obj) { var pass = true; - keys.forEach(function (key) { + keys.forEach(function(key) { if(key === 'and' || key === 'or') { if(Array.isArray(where[key])) { if(key === 'and') { @@ -461,6 +460,10 @@ function applyFilter(filter) { if (typeof value === 'string' && (example instanceof RegExp)) { return value.match(example); } + + if (example.regexp) + return value.match(example.regexp); + if (example === undefined) { return undefined; } diff --git a/lib/dao.js b/lib/dao.js index 58c93efb..ffc9622a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -939,7 +939,8 @@ var operators = { nin: 'NOT IN', neq: '!=', like: 'LIKE', - nlike: 'NOT LIKE' + nlike: 'NOT LIKE', + regexp: 'REGEXP' }; /* @@ -1169,6 +1170,13 @@ DataAccessObject._coerce = function (where) { throw err; } break; + case 'regexp': + val = utils.toRegExp(val); + if (val instanceof Error) { + result.statusCode = 400; + throw err; + } + break; } break; } @@ -1183,8 +1191,9 @@ DataAccessObject._coerce = function (where) { } } else { if (val != null) { - if (!((operator === 'like' || operator === 'nlike') && - val instanceof RegExp)) { + if (operator === 'regexp' && val instanceof RegExp) { + // do not coerce regex literals/objects + } else if (!((operator === 'like' || operator === 'nlike') && val instanceof RegExp)) { val = DataType(val); } } @@ -1237,6 +1246,7 @@ DataAccessObject._coerce = function (where) { * - neq: != * - like: LIKE * - nlike: NOT LIKE + * - regexp: REGEXP * * You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information. * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests. diff --git a/lib/utils.js b/lib/utils.js index 3398fabd..be72b626 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,6 +12,9 @@ exports.mergeQuery = mergeQuery; exports.mergeIncludes = mergeIncludes; exports.createPromiseCallback = createPromiseCallback; exports.uniq = uniq; +exports.toRegExp = toRegExp; +exports.hasRegExpFlags = hasRegExpFlags; +exports.getRegExpExpression = getRegExpExpression; var traverse = require('traverse'); var assert = require('assert'); @@ -506,3 +509,52 @@ function uniq(a) { } return uniqArray; } + +/** + * Converts a string, regex literal, or a RegExp object to a RegExp object. + * @param {String|Object} The string, regex literal, or RegExp object to convert + * @returns {Object} A RegExp object + */ +function toRegExp(regex) { + var isString = typeof regex === 'string'; + var isRegExp = regex instanceof RegExp; + + if (!(isString || isRegExp)) + return new Error('Invalid argument, must be a string, regex literal, or ' + + 'RegExp object'); + + if (isRegExp) + return regex; + + if (!hasRegExpFlags(regex)) + return new RegExp(regex); + + // only accept i, g, or m as valid regex flags + var flags = regex.split('/').pop().split(''); + var validFlags = ['i', 'g', 'm']; + var invalidFlags = []; + flags.forEach(function(flag) { + if (validFlags.indexOf(flag) === -1) + invalidFlags.push(flag); + }); + + var hasInvalidFlags = invalidFlags.length > 0; + if (hasInvalidFlags) + return new Error('Invalid regex flags: ' + invalidFlags); + + // strip regex delimiter forward slashes + var expression = regex.substr(1, regex.lastIndexOf('/') - 1); + return new RegExp(expression, flags.join('')); +} + +function hasRegExpFlags(regex) { + return regex instanceof RegExp ? + regex.toString().split('/').pop() : + !!regex.match(/.*\/.+$/); +} + +function getRegExpExpression(regex) { + return regex instanceof RegExp ? + regex.source : + regex.split('/').shift(); +} diff --git a/test/basic-querying.test.js b/test/basic-querying.test.js index e570dd0e..dabcccec 100644 --- a/test/basic-querying.test.js +++ b/test/basic-querying.test.js @@ -600,6 +600,24 @@ describe('basic-querying', function () { }); }); }); + + context('regexp operator', function() { + var invalidDataTypes = [0, true, {}, [], Function, null]; + + before(seed); + + it('should return an error for invalid data types', function(done) { + // `undefined` is not tested because the `removeUndefined` function + // in `lib/dao.js` removes it before coercion + invalidDataTypes.forEach(function(invalidDataType) { + User.find({where: {name: {regexp: invalidDataType}}}, function(err, + users) { + should.exist(err); + }); + }); + done(); + }); + }); }); function seed(done) { diff --git a/test/memory.test.js b/test/memory.test.js index c05be442..ca226e13 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -356,6 +356,34 @@ describe('Memory connector', function() { }); }); + it('should support the regexp operator with regex strings', function(done) { + User.find({where: {name: {regexp: '^J'}}}, function(err, users) { + should.not.exist(err); + users.length.should.equal(1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support the regexp operator with regex literals', function(done) { + User.find({where: {name: {regexp: /^J/}}}, function(err, users) { + should.not.exist(err); + users.length.should.equal(1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + + it('should support the regexp operator with regex objects', function(done) { + User.find({where: {name: {regexp: new RegExp(/^J/)}}}, function(err, + users) { + should.not.exist(err); + users.length.should.equal(1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); + it('should support nested property in query', function(done) { User.find({where: {'address.city': 'San Jose'}}, function(err, users) { should.not.exist(err); diff --git a/test/util.test.js b/test/util.test.js index c4e7eba6..440f18c9 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -420,3 +420,111 @@ describe('util.uniq', function() { }); }); + +describe('util.toRegExp', function() { + var invalidDataTypes; + var validDataTypes; + + before(function() { + invalidDataTypes = [0, true, {}, [], Function, null]; + validDataTypes = ['string', /^regex/, new RegExp(/^regex/)]; + }); + + it('should not accept invalid data types', function() { + invalidDataTypes.forEach(function(invalid) { + utils.toRegExp(invalid).should.be.an.Error; + }); + }); + + it('should accept valid data types', function() { + validDataTypes.forEach(function(valid) { + utils.toRegExp(valid).should.not.be.an.Error; + }); + }); + + context('with a regex string', function() { + it('should return a RegExp object when no regex flags are provided', + function() { + utils.toRegExp('^regex$').should.be.an.instanceOf(RegExp); + }); + + it('should throw an error when invalid regex flags are provided', + function() { + utils.toRegExp('^regex$/abc').should.be.an.Error; + }); + + it('should return a RegExp object when valid flags are provided', + function() { + utils.toRegExp('regex/igm').should.be.an.instanceOf(RegExp); + }); + }); + + context('with a regex literal', function() { + it('should return a RegExp object', function() { + utils.toRegExp(/^regex$/igm).should.be.an.instanceOf(RegExp); + }); + }); + + context('with a regex object', function() { + it('should return a RegExp object', function() { + utils.toRegExp(new RegExp('^regex$', 'igm')).should.be.an.instanceOf(RegExp); + }); + }); +}); + +describe('util.hasRegExpFlags', function() { + context('with a regex string', function() { + it('should be true when the regex has invalid flags', function() { + utils.hasRegExpFlags('^regex$/abc').should.be.ok; + }); + + it('should be true when the regex has valid flags', function() { + utils.hasRegExpFlags('^regex$/igm').should.be.ok; + }); + + it('should be false when the regex has no flags', function() { + utils.hasRegExpFlags('^regex$').should.not.be.ok; + utils.hasRegExpFlags('^regex$/').should.not.be.ok; + }); + }); + + context('with a regex literal', function() { + it('should be true when the regex has valid flags', function() { + utils.hasRegExpFlags(/^regex$/igm).should.be.ok; + }); + + it('should be false when the regex has no flags', function() { + utils.hasRegExpFlags(/^regex$/).should.not.be.ok; + }); + }); + + context('with a regex object', function() { + it('should be true when the regex has valid flags', function() { + utils.hasRegExpFlags(new RegExp(/^regex$/igm)).should.be.ok; + }); + + it('should be false when the regex has no flags', function() { + utils.hasRegExpFlags(new RegExp(/^regex$/)).should.not.be.ok; + }); + }); +}); + +describe('util.getRegExpExpression', function() { + context('with a regex string', function() { + it('should return the expression without flags', function() { + utils.getRegExpExpression('^regex$/abc').should.equal('^regex$'); + }); + }); + + context('with a regex literal', function() { + it('should return the expression without flags', function() { + utils.hasRegExpFlags(/^regex$/igm).should.be.ok; + }); + }); + + context('with a regex object', function() { + it('should return the expression without flags', function() { + utils.hasRegExpFlags(new RegExp(/^regex$/igm)).should.be.ok; + }); + }); +});