We need a way to let a user specify the content of a Mongo query, for filtering purposes. For example, when displaying a list of products to a user, they might want to filter by price and when the product was added:
{
price: { $gte: 3 },
added: { $lt: new Date(Date.now()-86400000) },
}
This Meteor package allows us to specify a "filter specification" containing information on exactly what values an end-user is allowed to supply for a particular filter.
var ProductFilter = Filter.create({
filters: {
MinPrice: function (minPrice) {
return {
price: {
$gte: minPrice
}
};
},
AddedBefore: function (addedBefore) {
return {
added: {
$lt: addedBefore
}
};
}
}
});
var filter = new ProductFilter();
filter.set({
MinPrice: 3
});
filter.set({
AddedBefore: new Date(Date.now()-86400000)
});
var mongoQuery = filter.query();
// {
// price: { '$gte': 3 },
// added: { '$lt': Date('Sun Dec 20 2015 12:29:19 GMT+0000 (GMT)') }
// }
var serialized = filter.save();
// {
// MinPrice: 3,
// AddedBefore: Date('Sun Dec 20 2015 12:29:19 GMT+0000 (GMT)')
// }
The serialized
object can be used to recreate a filter with the same options:
var filter = new ProductFilter(serialized);
If any of the values in the serialized
object are invalid, then an Error
object is thrown.
We also provide a number of "filter factory functions" in the Filter Object so you don't need to write snippets of Mongo queries. The above ProductFilter
could be rewritten as:
var ProductFilter = Filter.create({
filters: {
MinPrice: Filter.Gte('price'),
AddedBefore: Filter.Lt('added')
}
});
As well as being less code, these factory functions do additional validation. For example, Filter.Gte
and Filter.Lt
both check that the value passed in is either a Date
Object or a Number
. If not, they throw an Error
object.
You may want to write your own such factory functions for common filter types. For example, if you need to filter based on a date being between two other dates:
var DateBetweenFilter = function DateBetweenFilterFactory(field) {
return function DateBetweenFilter(value) {
if (typeof value !== 'object'
|| !(value.before instanceof Date)
|| !(value.after instanceof Date)
) {
throw new Error('Invalid value passed to DateBetweenFilter');
}
var query = {};
query[field] = {
$gt: value.after,
$lt: value.before
};
return query;
};
};
var ProductFilter = Filter.create({
filters: {
MinPrice: Filter.Gte('price'),
AddedBetween: DateBetweenFilter('added'),
}
});
var filter = new ProductFilter();
filter.set({
MinPrice: 3,
AddedBetween: {
after: new Date(Date.now()-86400000),
before: new Date(Date.now()+86400000)
}
});
var mongoQuery = filter.query();
// {
// price: { '$gte': 3 },
// added: {
// '$gt': Date('Sun Dec 20 2015 12:29:19 GMT+0000 (GMT)')
// '$lt': Date('Sun Dec 22 2015 12:29:19 GMT+0000 (GMT)')
// }
// }
var serialized = filter.save();
// {
// MinPrice: 3,
// AddedBetween: {
// after: Date('Sun Dec 20 2015 12:29:19 GMT+0000 (GMT)')
// before: Date('Sun Dec 22 2015 12:29:19 GMT+0000 (GMT)')
// }
// }
The spec passed to the constructor contains a series of key/values, where the key is the name for this part of the filter, and the value is a filter function which converts a value into a Mongo query. Instead of the value being a function, it can also be an Object containing a "filter" item. These are equivalent:
var ProductFilter = Filter.create({
filters: {
MinPrice: Filter.Gte('price')
}
});
var ProductFilter = Filter.create({
filters: {
MinPrice: {
filter: Filter.Gte('price')
}
}
});
var ProductFilter = Filter.create({
filters: [
{
MinPrice: Filter.Gte('price')
}
]
});
var ProductFilter = Filter.create([
{
MinPrice: Filter.Gte('price')
}
]);
The first version is compact, but the second version allows us to store more things alongside the filter, such as arbitrary useful meta data. The third version uses an Array. The only purpose of this is so we can recover the order that the filters were defined in when calling ProductFilter.names()
. The fourth version is a
more compact version of the third. So why the third? In case we
want to pass things other than filters
, such as type
:
As well as passing filters
to Filter.create, you can also pass
an optional type
parameter:
var ProductFilter = Filter.create({
type: 'Foo',
filters: {
...
}
});
If you do this, then you can call the type function on the filter spec object, or any filters created from it, to get that type. This allows you to uniquely identify filter specs without having to pass names around:
ProductFilter.type() === 'Foo'; // true
var filter = new ProductFilter();
filter.type() === 'Foo'; // true
Here we store some meta data information about a "template" that we might want to use to let the user select what min-price to filter on:
var ProductFilter = Filter.create({
filters: {
MinPrice: {
filter: Filter.Gte('price'),
meta: {
template: Template.minPriceFilterTemplate
}
}
},
});
This meta data can be extracted from the ProductFilter using either of the following methods:
var minPriceTemplate = ProductFilter.meta('MinPrice').template;
var minPriceTemplate = ProductFilter.meta().MinPrice.template;
"template" is arbitrary. You can store whatever data you want in there.
Another way of storing meta data is to store it as a value on the actual filter function. The two meta data locations are merged at the top level with the one discussed first taking priority. So for example:
function CustomFilter (field) {
var filter_func = function (value) {
var q = {};
q[ field ] = value * 2;
return q;
};
filter_func.meta = {
foo: 123,
bar: 234,
};
return filter_func;
};
var ProductFilter = Filter.create({
filters: {
MinPrice: {
filter: CustomFilter('price'),
meta: {
bar: 789,
}
}
}
});
ProductFilter.meta('MinPrice') ==
{
foo: 123,
bar: 789,
}
"meta" is a setter as well as a getter. You can shallow-merge in additional meta data after the spec is created. For example:
var ProductFilter = Filter.create({
filters: {
MinPrice: CustomFilter('price')
}
});
ProductFilter.meta('MinPrice', {
bar: 789
});
To set multiple values at once:
var ProductFilter = Filter.create({
filters: {
MinPrice: CustomFilter('price'),
MaxPrice: SomeOtherFilter('price'),
}
});
ProductFilter.meta({
MinPrice: { bar: 789 },
MaxPrice: { foo: 'abc' },
});
Another optional item you can pass is beforeSet
. This is a function
which takes the value being set and can run arbitrary tasks before the set happens. It can also optionally change the value being set. For
example
var ProductFilter = Filter.create({
filters: {
MinPrice: {
filter: CustomFilter('price'),
beforeSet: function (value, callback) {
var low = ProductFilter.meta('MinPrice').low;
if (value < low) {
callback(low);
}
}
}
}
});
You don't have to run the callback to change the value, but if you do want to do that, it must happen immediately during the same event loop tick. I.e, this wouldn't work:
if (value < low) {
setTimeout(function(){
callback(low);
}, 1);
}
There is also a similar beforeUnset
function. This is not passed any arguments at all. You can access this.name
and this.filter
You can call the names
function on either the ProductFilter class or an instance of it in order to get a list of names of filters which are available. If you passed the Filter spec as an Array, then the values will be returned in the same order. For example:
var ProductFilter = Filter.create([
{ First: ... },
{ Second: ... },
]);
var filter = new ProductFilter();
ProductFilter.names() == ['First', 'Second'];
filter.names() == ['First', 'Second'];
If we hadn't used Array syntax when creating the ProductFilter class, then the returned Array may have been ['Second', 'First']
Now we have ProductFilter, we want to create an instance of it which we can set values for MinPrice on.
var filter = new ProductFilter({ MinPrice: 3 });
Here we instantiated a filter object with MinPrice set to 3. We could have refrained from passing an Object to not set a value for MinPrice, and then set it later with one of these two calls:
var filter = new ProductFilter();
filter.set('MinPrice', 3);
filter.set({ 'MinPrice': 3 });
The second notation is useful when you want to set more than one value at the same time. There is an "unset" function to remove one or more values:
filter.unset('MinPrice');
filter.unset('Multiple', 'Values');
filter.unset(['Multiple', 'Values', 'In', 'An', 'Array']);
To clone a filter object:
var newFilter = filter.clone();
To automatically set new values to a cloned version of a filter, you can treat clone
like set
:
var filter = new ProductFilter({ MinPrice: 3 });
var newFilter = filter.clone({ MaxPrice: 10 });
After running the above, newFilter would contain a filter with MinPrice set to 3 and MaxPrice set to 10.
To clear all values:
filter.clear();
To reset the object data to what it was when it was originally constructed:
filter.reset();
If any arguments are passed to clear
or reset
, they are proxied through to set
after the clear
or reset
operation is performed. A call to these functions will only trigger a maximum of one change
event, and only if the final result is different to the original.
To get the data which has been set on the filter:
filter.set('Foo', 123);
filter.set('Bar', [1, 2, 3]);
var result = filter.save();
result == {
Foo: 123,
Bar: [1, 2, 3]
}
To get the mongo query:
var mongoQuery = filter.query();
The save
, query
and get
functions are reactive. So if you do this:
var filter = new ProductFilter({ MinPrice: 3 });
Tracker.autorun(function(){
console.log("Filter query:", filter.query());
});
The filter query will be printed to the console immediately, and whenever it is changed.
A direct "equals" comparison. $eq
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | String or Number |
Value |
var filter = Filter.Eq('price');
var result = filter(3);
result == {
'price': 3
};
A direct "not equals" comparison. $eq
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | String or Number |
Value |
var filter = Filter.Ne('price');
var result = filter(3);
result == {
'price': { $ne: 3 }
};
A "greater than" comparison. $gt
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Number |
Value |
var filter = Filter.Gt('price');
var result = filter(3);
result == {
'price': {
$gt: 3
}
};
A "greater than or equal to" comparison. $gte
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Number |
Value |
var filter = Filter.Gte('price');
var result = filter(3);
result == {
'price': {
$gte: 3
}
};
A "less than" comparison. $lt
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Number |
Value |
var filter = Filter.Lt('price');
var result = filter(3);
result == {
'price': {
$lt: 3
}
};
A "less than or equal to" comparison. $lte
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Number |
Value |
var filter = Filter.Lte('price');
var result = filter(3);
result == {
'price': {
$lte: 3
}
};
A "must be one of" comparison. $in
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array |
Value |
var filter = Filter.In('price');
var result = filter([2, 3, 4]);
result == {
'price': {
$in: [2, 3, 4]
}
};
A "must not be one of" comparison. $nin
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array |
Value |
var filter = Filter.Nin('price');
var result = filter([2, 3, 4]);
result == {
'price': {
$nin: [2, 3, 4]
}
};
A Boolean OR filter. $or
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array OR Object |
Value |
var filter = Filter.Or([
Filter.Eq('price')(0),
Filter.Gt('price')(4)
]);
var result = filter();
result == {
$or: [
{ price: 0 },
{ price: { $gt: 4 } }
]
};
In the above example, we are hard coding the two values inside the factory function. This is not ideal. You can alternatively use Object notiation instead of Array notation in order to pass values through from the filter function. The following would produce the exact same result as above:
var filter = Filter.Or({
ExactPrice: Filter.Eq('price'),
GtPrice: Filter.Gt('price')
});
var result = filter({
ExactPrice: 0,
GtPrice: 4
});
You can create queries of arbitrary depth using this method:
var filter = Filter.Or({
price: Filter.Or({
exact: Filter.Eq('price'),
above: Filter.Gt('price'),
}),
status: Filter.Eq('status'),
});
var result = filter({
price: {
exact: 10,
above: 20
},
status: 'active'
});
result == {
$or: [
{
$or: [
{
price: 10,
},
{
price: { $gt: 20 }
}
]
},
{
status: 'active',
}
]
};
It would also have been possible to shift the "active" status into the factory function above, so it doesn't need to be specified when the filter function is called:
var filter = Filter.Or({
price: Filter.Or({
exact: Filter.Eq('price'),
above: Filter.Gt('price'),
}),
status: Filter.Eq('status')('active'),
});
var result = filter({
price: {
exact: 10,
above: 20
}
});
A Boolean AND filter. $and
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array OR Object |
Value |
See Filter.Or. It works exactly the same except uses $and instead of $or
A "must not be" comparison. $not
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | anything | Value |
var filter = Filter.Not(Filter.Eq('price'));
var result = filter(5);
result == {
'price': {
$not: {
$eq: 5
}
}
};
The filter function provided by the Filter.Not factory function, passes the value supplied to its child.
The Mongo $not filter is strange in that it is incompatible with certain queries. The Filter.Not factory function tries to restructure things to make them work. For example, the following 3 are invalid:
1. { price: { $not: 3 } }
2. { name: { $not: { $regexp: /^A/, $options: 'i' } } }
3. { $where: { $not: someFunction } }
So they are converted to these valid versions:
1. { price: { $not: { $eq: 3 } } }
2. { name: { $not: /^A/i } }
3. { $where: function() { return !(someFunction.apply(this, arguments)) } }
A Boolean NOT OR filter. $nor
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array OR Object |
Value |
See Filter.Or. It works exactly the same except uses $nor instead of $or
A "exists" comparison. $exists
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Boolean |
Optional. Defaults to true |
var filter = Filter.Exists('price');
var result = filter(false);
result == {
'price': {
$exists: false
}
};
result = filter();
result == {
'price': {
$exists: true
}
};
A "must be of this type" comparison. $type
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Factory arg2 | Number |
Field value type |
var filter = Filter.Type('date', 9);
var result = filter();
result == {
'date': {
$type: 9
}
};
A "given this divisor, must match this remainder" comparison. $mod
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Object |
Contains divisor and remainder |
var filter = Filter.Mod('price');
var result = filter({ divisor: 2, remainder: 1 });
result == {
'price': {
$mod: [ 2, 1 ]
}
};
The above would pick out all documents where the "price" value is an odd number (1, 3, 5 etc).
A "must match this regular expression" comparison. $regex
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Factory arg2 | String or RegExp |
Regular expression |
Factory arg3 | String |
Optional regex specifiers |
var filter = Filter.Regex('name', '^A', 'i'); // or:
var filter = Filter.Regex('name', /^A/, 'i'); // or:
var filter = Filter.Regex('name', /^A/i);
var result = filter();
result == {
'name': {
$regex: /^A/i
}
};
A "must match this text" comparison. $text
Type | Description | |
---|---|---|
Factory arg1 | String |
Language (optional) |
Function arg1 | String |
String to search for |
var filter = Filter.Text('en');
var result = filter('Mike Cardwell');
result == {
$text: {
$search: 'Mike Cardwell',
$language: 'en'
}
};
Unlike the other filter factories we've looked at up until this point, we don't specify a field name. That is because Mongo searches any field with a text index in the collection.
A "function run on document must return true" comparison. $where
Type | Description | |
---|---|---|
Factory arg1 | Function or String |
Function to run on documents |
Function args | anything | All args passed to function supplied in Factory arg1 |
var filter = Filter.Where(function(field, min){
return this[ field ] >= min;
});
var result = filter('price', 3);
result == {
$where: function () {
return this.price >= 3;
}
};
A "all values must be contained in doc" comparison. $all
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Array |
Values |
var filter = Filter.All('names');
var result = filter(['Mike', 'Cardwell']);
result == {
names: {
$all: [
'Mike',
'Cardwell'
]
}
};
A "at least one element query must match doc" comparison. $elemMatch
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Object |
Queries |
var filter = Filter.ElemMatch('price');
var result = filter({
$lt: 5,
$gt: 10
});
result == {
price: {
$elemMatch: {
$lt: 5,
$gt: 10
}
}
};
A "array must have this many items" comparison. $size
Type | Description | |
---|---|---|
Factory arg1 | String |
Field name |
Function arg1 | Number |
Number of items |
var filter = Filter.Size('names');
var result = filter(2);
result == {
names: {
$size: 2
}
};
There are a tests for the filter factory functions. You can run them by doing this:
cd tests
npm install
npm test