Add support for regex operator

This commit is contained in:
Simon Ho 2015-07-24 12:56:31 -07:00
parent 01af886c7d
commit b8f1598723
6 changed files with 226 additions and 7 deletions

View File

@ -350,9 +350,8 @@ Memory.prototype.all = function all(model, filter, options, callback) {
} }
// do we need some filtration? // do we need some filtration?
if (filter.where) { if (filter.where && nodes)
nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes; nodes = nodes.filter(applyFilter(filter));
}
// field selection // field selection
if (filter.fields) { if (filter.fields) {
@ -401,7 +400,7 @@ function applyFilter(filter) {
var keys = Object.keys(where); var keys = Object.keys(where);
return function (obj) { return function (obj) {
var pass = true; var pass = true;
keys.forEach(function (key) { keys.forEach(function(key) {
if(key === 'and' || key === 'or') { if(key === 'and' || key === 'or') {
if(Array.isArray(where[key])) { if(Array.isArray(where[key])) {
if(key === 'and') { if(key === 'and') {
@ -461,6 +460,10 @@ function applyFilter(filter) {
if (typeof value === 'string' && (example instanceof RegExp)) { if (typeof value === 'string' && (example instanceof RegExp)) {
return value.match(example); return value.match(example);
} }
if (example.regexp)
return value.match(example.regexp);
if (example === undefined) { if (example === undefined) {
return undefined; return undefined;
} }

View File

@ -939,7 +939,8 @@ var operators = {
nin: 'NOT IN', nin: 'NOT IN',
neq: '!=', neq: '!=',
like: 'LIKE', like: 'LIKE',
nlike: 'NOT LIKE' nlike: 'NOT LIKE',
regexp: 'REGEXP'
}; };
/* /*
@ -1169,6 +1170,13 @@ DataAccessObject._coerce = function (where) {
throw err; throw err;
} }
break; break;
case 'regexp':
val = utils.toRegExp(val);
if (val instanceof Error) {
result.statusCode = 400;
throw err;
}
break;
} }
break; break;
} }
@ -1183,8 +1191,9 @@ DataAccessObject._coerce = function (where) {
} }
} else { } else {
if (val != null) { if (val != null) {
if (!((operator === 'like' || operator === 'nlike') && if (operator === 'regexp' && val instanceof RegExp) {
val instanceof RegExp)) { // do not coerce regex literals/objects
} else if (!((operator === 'like' || operator === 'nlike') && val instanceof RegExp)) {
val = DataType(val); val = DataType(val);
} }
} }
@ -1237,6 +1246,7 @@ DataAccessObject._coerce = function (where) {
* - neq: != * - neq: !=
* - like: LIKE * - like: LIKE
* - nlike: NOT 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. * 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. * @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests.

View File

@ -12,6 +12,9 @@ exports.mergeQuery = mergeQuery;
exports.mergeIncludes = mergeIncludes; exports.mergeIncludes = mergeIncludes;
exports.createPromiseCallback = createPromiseCallback; exports.createPromiseCallback = createPromiseCallback;
exports.uniq = uniq; exports.uniq = uniq;
exports.toRegExp = toRegExp;
exports.hasRegExpFlags = hasRegExpFlags;
exports.getRegExpExpression = getRegExpExpression;
var traverse = require('traverse'); var traverse = require('traverse');
var assert = require('assert'); var assert = require('assert');
@ -506,3 +509,52 @@ function uniq(a) {
} }
return uniqArray; 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();
}

View File

@ -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) { function seed(done) {

View File

@ -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) { it('should support nested property in query', function(done) {
User.find({where: {'address.city': 'San Jose'}}, function(err, users) { User.find({where: {'address.city': 'San Jose'}}, function(err, users) {
should.not.exist(err); should.not.exist(err);

View File

@ -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;
});
});
});