Skip to content

Commit

Permalink
Merge pull request #7 from seangarner/case-insensitive-eq
Browse files Browse the repository at this point in the history
support for case insensitive matching
  • Loading branch information
seangarner committed Oct 22, 2015
2 parents a276a75 + e55b5a5 commit 3986faa
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# mongo-url-parser changelog

## NEXT MINOR

- add `caseInsensitiveOperators` `query` option
+ enables case insensitive matching for `eq`, `ne`, `contains`, `startsWith`, and `endsWith`

## 1.1.1 (2015/07/22)

- fixed not being able to disable `contains`, `startsWith`, and `endsWith`
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,25 @@ contains | `contains(borough, "shire")`
The extra operators also support `$not`. For example `not(contains(borough, "shire"))` would
find the docs in which `borough` does not contain `shire`.

#### case insensitive matching
Some operators support the `i` flag to denote that the operator should match the value case
insensitively. This is useful if you want to enable case insensitive match without allowing
full `$regex` powers (because `$regex` is the only way of achieving this in mongo).

- `eq(tags, 'NODE', i)` matches Node, NODE, node, NoDe, etc

Also supported with `ne`, `startsWith`, `endsWith` and `contains`, but must be enabled using the
`disabledOperators` query option as the default is to disable this feature.

```js
var options = {
query: {
caseInsensitiveOperators: true
}
};
mongoUrlUtils({query: 'regex(email,"[email protected]",i)'}, options);
```

#### mongo types
The `type()` query operator allows either integer identifiers as per the mongodb documentation. For
convinience it also maps the following types to their ids: `Double`, `String`, `Object`, `Array`,
Expand Down
109 changes: 94 additions & 15 deletions src/query.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//TODO: disabled presets (e.g. mongo 2.2/2.6/3.0)
//TODO: determine dependencies automatically
if (!Array.isArray(options.disabledOperators)) options.disabledOperators = [];
if (options.caseInsensitiveOperators === undefined) options.caseInsensitiveOperators = false;

function collect(head, tail) {
var res = [head];
Expand All @@ -11,6 +12,17 @@
return res;
}

function contains(a, v) {
return (Array.isArray(a) && a.indexOf(v) > -1);
}

function uniq(a) {
return a.reduce(function (memo, v) {
if (!contains(memo, v)) memo.push(v);
return memo;
}, []);
}

function set(o, p, v) {
o[p] = v;
return o;
Expand All @@ -25,14 +37,31 @@
throw new Error(keyword + ' operator is disabled');
}
}

function assertCanWithSwitches(operator, switches) {
if (switches === null) return true;
switches.forEach(function (sw) {
switch (sw) {
case 'i':
if (!options.caseInsensitiveOperators) {
throw new Error(sw + ' switch is disabled for ' + operator + ' operator');
}
break;
default:
throw new Error(sw + ' switch is unrecognised for ' + operator + ' operator');
}
});
}
}

start
= Query

Query
= ScalarComparison
= Eq
/ ScalarComparison
/ LogicalComparison
/ Ne
/ ArrayComparison
/ Exists
/ ElemMatch
Expand All @@ -50,14 +79,17 @@ Query
//TODO: /regex/ (can't use $regex with $in/$nin)

ScalarComparisonOperator
= "eq"
/ "gte"
= "gte"
/ "gt"
/ "lte"
/ "lt"
/ "ne"
/ "size"

EqualityComparisonOperator
= "eq"
/ "ne"

ArrayComparisonOperator
= "in"
/ "nin"
Expand Down Expand Up @@ -94,37 +126,78 @@ Regex
return set({}, prop, {$regex: pattern});
}

Eq
= "eq(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ ")" {
var child = {};
assertCan('eq');
if (switches) {
assertCanWithSwitches('eq', switches);
child = new RegExp('^' + escapeRegex(value) + '$', switches);
} else {
child = {$eq: value};
}
return set({}, prop, child);
}

Ne
= "ne(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ ")" {
var child = {};
assertCan('ne');
if (switches) {
assertCanWithSwitches('ne', switches);
child = {$not: new RegExp('^' + escapeRegex(value) + '$', switches)};
} else {
child = set({}, '$ne', value);
}
return set({}, prop, child);
}

StartsWith
= "startsWith(" __ prop:Property __ "," __ value:Scalar __ ")" {
= "startsWith(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ ")" {
assertCan('startsWith');
return set({}, prop, {$regex: '^' + escapeRegex(value)});
assertCanWithSwitches('startsWith', switches);
value = {$regex: '^' + escapeRegex(value)};
if (contains(switches, 'i')) value.$options = 'i';
return set({}, prop, value);
}
/ "not(startsWith(" __ prop:Property __ "," __ value:Scalar __ "))" {
/ "not(startsWith(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ "))" {
assertCan('startsWith');
assertCan('not');
return set({}, prop, {$not: new RegExp('^' + escapeRegex(value))});
assertCanWithSwitches('startsWith', switches);
var flags = (switches || []).join('');
return set({}, prop, {$not: new RegExp('^' + escapeRegex(value), flags)});
}

EndsWith
= "endsWith(" __ prop:Property __ "," __ value:Scalar __ ")" {
= "endsWith(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ ")" {
assertCan('endsWith');
return set({}, prop, {$regex: escapeRegex(value) + '$'});
assertCanWithSwitches('endsWith', switches);
value = {$regex: escapeRegex(value) + '$'};
if (contains(switches, 'i')) value.$options = 'i';
return set({}, prop, value);
}
/ "not(endsWith(" __ prop:Property __ "," __ value:Scalar __ "))" {
/ "not(endsWith(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ "))" {
assertCan('endsWith');
assertCan('not');
return set({}, prop, {$not: new RegExp(escapeRegex(value) + '$')});
assertCanWithSwitches('endsWith', switches);
var flags = (switches || []).join('');
return set({}, prop, {$not: new RegExp(escapeRegex(value) + '$', flags)});
}

Contains
= "contains(" __ prop:Property __ "," __ value:Scalar __ ")" {
= "contains(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ ")" {
assertCan('contains');
return set({}, prop, {$regex: escapeRegex(value)});
assertCanWithSwitches('contains', switches);
value = {$regex: escapeRegex(value)};
if (contains(switches, 'i')) value.$options = 'i';
return set({}, prop, value);
}
/ "not(contains(" __ prop:Property __ "," __ value:Scalar __ "))" {
/ "not(contains(" __ prop:Property __ "," __ value:Scalar __ switches:Switches? __ "))" {
assertCan('contains');
assertCan('not');
return set({}, prop, {$not: new RegExp(escapeRegex(value))});
assertCanWithSwitches('contains', switches);
var flags = (switches || []).join('');
return set({}, prop, {$not: new RegExp(escapeRegex(value), flags)});
}

Where
Expand Down Expand Up @@ -185,6 +258,12 @@ Type
return set({}, prop, {$type: id});
}

// switches used by eq, ne, contains, startswith, endswith
Switches
= "," __ switches:[i]+ __? {
return uniq(switches);
}

MongoType
= ParsedInt
/ "Double"
Expand Down
13 changes: 11 additions & 2 deletions test/integration/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ var tests = [
['eq(closed,"true")', []],
['eq(closed,true)', [3]],
['eq(grades.score,5)', [4,7,8]],
['eq(name,"WEST AND SONS")', []],
['eq(name,"WEST AND SONS", i)', [1]],
['gte(id,6)', [6,7,8,9,10]],
['gt(id,6)', [7,8,9,10]],
['lte(id,3)', [1,2,3]],
['lt(id,3)', [1,2]],
['ne(closed,true)', [1,2,4,5,6,7,8,9,10]],
['ne(name,"WEST AND SONS")', [1,2,3,4,5,6,7,8,9,10]],
['ne(name,"WEST AND SONS",i)', [2,3,4,5,6,7,8,9,10]],
['and(eq(grades.score,5),eq(borough,"Buckinghamshire"))', [4,8]],
['or(eq(id,1),eq(borough,"Buckinghamshire"))', [1,4,5,8]],
['or(eq(id,1),and(gt(id,5),lt(id,7)))', [1,6]],
Expand All @@ -30,13 +33,19 @@ var tests = [
['text("& son")', [1]],
['text("& son", "es")', []],
['endsWith(borough,"shire")', [3,4,5,6,7,8,10]],
['endsWith(borough,"SHIRE",i)', [3,4,5,6,7,8,10]],
['endsWith(borough,"s.*e")', []],
['startsWith(name,"W")', [1,7]],
['startsWith(name,"w",i)', [1,7]],
['contains(name,"and")', [1,3,5,6,8,10]],
['contains(name,"AND",i)', [1,3,5,6,8,10]],
['contains(name,".*and.*")', []],
['not(startsWith(name,"W"))', [2,3,4,5,6,8,9,10]],
['not(startsWith(name,"w",i))', [2,3,4,5,6,8,9,10]],
['not(endsWith(borough,"shire"))', [1,2,9]],
['not(endsWith(borough,"SHIRE",i))', [1,2,9]],
['not(contains(name,"and"))', [2,4,7,9]],
['not(contains(name,"AND",i))', [2,4,7,9]],
['type(name,String)', [1,2,3,4,5,6,7,8,9,10]],
['type(name,Object)', []],
['type(closed,Boolean)', [3,6]],
Expand Down Expand Up @@ -78,7 +87,7 @@ describe('query integration test:', function() {
tests.forEach(function (test) {
var q = test[0];
it(q, function (done) {
var qry = query(q);
var qry = query(q, {caseInsensitiveOperators: true});
if (DEBUG) console.dir(query(q));
docs.find(qry, {sort: {id: 1}}).toArray(function (err, docs) {
if (err) return done(err);
Expand Down
46 changes: 46 additions & 0 deletions test/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,52 @@ describe('query', function() {
.to.throw('endsWith operator is disabled');
});
});

describe('caseInsensitiveOperators', function() {

it('should throw an error when false and `i` switch is used', function () {
expect(query.bind(null, 'eq(tags,"node",i)', {caseInsensitiveOperators:false})).to.throw('i switch is disabled for eq');
expect(query.bind(null, 'ne(tags,"node",i)', {caseInsensitiveOperators:false})).to.throw('i switch is disabled for ne');
expect(query.bind(null, 'startsWith(tags,"node",i)', {caseInsensitiveOperators:false})).to.throw('i switch is disabled for startsWith');
expect(query.bind(null, 'endsWith(tags,"node",i)', {caseInsensitiveOperators:false})).to.throw('i switch is disabled for endsWith');
expect(query.bind(null, 'contains(tags,"node",i)', {caseInsensitiveOperators:false})).to.throw('i switch is disabled for contains');
});

it('should default to off', function () {
expect(query.bind(null, 'eq(tags,"node",i)')).to.throw('i switch is disabled for eq');
expect(query.bind(null, 'ne(tags,"node",i)')).to.throw('i switch is disabled for ne');
expect(query.bind(null, 'startsWith(tags,"node",i)')).to.throw('i switch is disabled for startsWith');
expect(query.bind(null, 'endsWith(tags,"node",i)')).to.throw('i switch is disabled for endsWith');
expect(query.bind(null, 'contains(tags,"node",i)')).to.throw('i switch is disabled for contains');
});

it('should make the operator return a "safe" $regex operation', function () {
expect(query('eq(tags,"NODE",i)', {caseInsensitiveOperators:true})).to.deep.eql({
tags: /^NODE$/i
});
expect(query('ne(tags,"NODE",i)', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$not: /^NODE$/i}
});
expect(query('startsWith(tags,"NODE",i)', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$regex: '^NODE', $options: 'i'}
});
expect(query('not(startsWith(tags,"NODE",i))', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$not: /^NODE/i}
});
expect(query('endsWith(tags,"NODE",i)', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$regex: 'NODE$', $options: 'i'}
});
expect(query('not(endsWith(tags,"NODE",i))', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$not: /NODE$/i}
});
expect(query('contains(tags,"NODE",i)', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$regex: 'NODE', $options: 'i'}
});
expect(query('not(contains(tags,"NODE",i))', {caseInsensitiveOperators:true})).to.deep.eql({
tags: {$not: /NODE/i}
});
});
});
});

describe('regex operator', function() {
Expand Down

0 comments on commit 3986faa

Please sign in to comment.