meteor: collections by reference
You may find yourself in a situation where you need to modify a collection by name, or write a function which performs a generic operation on any collection. In these cases, you'll need to understand how to dynamically access a meteor collection instance by reference. In this post we'll explore these concepts with a couple of simple examples.
Call with a reference
Let's say you need to increment fields in your documents like this:
Games.update(gameId, {$inc: {score: 10}});
Videos.update(videoId, {$inc: {'stats.views': 1}});
Instead, you'd prefer to have a generic increment
function which could work with any collection:
increment(Games, gameId, 'score', 10);
increment(Videos, videoId, 'stats.views', 1);
Here's how we could implement increment
:
var increment = function(collection, id, field, amount) {
// build a modifier like {$inc: {score: 10}}
var modifier = {$inc: {}};
modifier.$inc[field] = amount;
// update using a reference to a collection instance
return collection.update(id, modifier);
};
The first part of the function handles the mechanics of the $inc
modifier, but the last line shows how the collection reference is used. Remember that Games
and Videos
are just objects, and in javascript, objects are passed by reference.
Call with a name
Now let's say you need a different version of increment
that uses the collection's name instead:
increment('Games', gameId, 'score', 10);
increment('Videos', videoId, 'stats.views', 1);
Again we'll need to get a reference to a collection instance, but this time we'll use the name to find it in the global context. The context, however, varies depending on the environment in which the code is executing. On the client it's window
, but on the server it's global
. Here's a new implementation:
var increment = function(collectionName, id, field, amount) {
// choose the global context based on the environment
var root = Meteor.isClient ? window : global;
// find the instance in the global context - e.g. window['Games']
var collection = root[collectionName];
// this part is unchanged from the previous example
var modifier = {$inc: {}};
modifier.$inc[field] = amount;
return collection.update(id, modifier);
};
Keep in mind that collectionName
needs to have the exact spelling and capitalization as the variable name.
Security
The aforementioned procedure should be used with extreme caution in publishers and methods. If you fail to check your inputs, clients could trick your server into doing something unpleasant. Imagine we added an increment
method:
Meteor.methods({
increment: function(collectionName, id, field, amount) {
check(collectionName, String);
check(id, String);
check(field, String);
check(amount, Number);
return increment(collectionName, id, field, amount);
}
});
That looks innocent enough,but consider what would happen if a user was sneaky:
Meteor.call('increment', 'Users', Meteor.userId(), 'isPaid', 1);
The user could set his isPaid
value to something truthy which could circumvent the app's payment mechanism. We must validate that the collection name is in the allowed set. Fortunately, this is easy to do with a Match.Where:
check(collectionName, Match.Where(function(name) {
return _.contains(['Games', 'Videos'], name);
}));
Conclusion
And that's it! It's a pretty simple technique, but hopefully you'll find it useful in your own projects.