If there’s one thing every programmer hates, it’s code duplication. If there’s two things every programmer hates, it’s code duplication and insecure code.

So today, let me show you a pattern that helps with both problems at the same time.

A Simple Publication

To understand what this pattern does, let’s start by considering a simple publication that returns the 10 latest posts:

// server
Meteor.publish('posts', function() {
  return Posts.find({}, {sort: {createdAt: -1}, limit: 10});
});

On the client, we’ll subscribe (in the router, or in a template):

// client
Meteor.subscribe('posts');

And finally, query for our posts:

// client
var latestPosts = Posts.find();

Now while this technically works in our simplified scenario, as soon as – for whatever reason – we subscribe to more than these 10 posts, we’ll have no guarantee anymore that the find() call will return the 10 latest posts. Which is why we add find and sort specifiers on the client, too:

// client
var latestPosts = Posts.find({}, {sort: {createdAt: -1}, limit: 10});

This is where code duplication starts to appear: the same Posts.find() code snippet is now repeated on the client, and in the publication. Let’s see what we can do about it.

The Wrong Solution

We could decide to only define our Posts.find() requirements on the client, and from there pass them on to the server. It would look something like this:

// server
Meteor.publish('posts', function(parameters) {
  return Posts.find(parameters.find, parameters.options);
});

And on the client:

// client
var parameters = {
  find: {},
  options: {sort: {createdAt: -1}, limit: 10}
};

Meteor.subscribe('posts', parameters);
var latestPosts = Posts.find(parameters.find, parameters.options);

The problem here is that we’ve just opened a big security hole. While we know our app will only pass sane, expected value for parameters.find and parameters.options, we can’t say the same about the client in general.

Nothing is preventing a user from opening their browser console and subscribing with their own custom parameters.find argument that will expose your entire collection, whether you want it or not!

Centralizing Parameters

We can’t trust the client, so instead what if we used a central object, shared between client and server, to hold our Mongo parameters? It could look something like this:

// client & server
latestPost = {
  find: {},
  options: {sort: {createdAt: -1}, limit: 10}
}

// server
Meteor.publish('posts', function() {
  return Posts.find(latestPost.find, latestPost.options);
});

// client
Meteor.subscribe('posts');
var latestPosts = Posts.find(latestPost.find, latestPost.options);

This works, but using a hard-coded object isn’t very flexible. For example, what if we wanted to let the client specify its own limit parameter to subscribe to 10, 20, or 30 posts?

Query Constructors

So let’s try again, but this time with a function instead of an object:

// client & server
latestPost = function (limit) {
  return {
    find: {},
    options: {sort: {createdAt: -1}, limit: limit}
  };
}

// server
Meteor.publish('posts', function(limit) {
  return Posts.find(latestPost(limit).find, latestPost(limit).options);
});

// client
var limit = 20;
Meteor.subscribe('posts', limit);
var latestPosts = Posts.find(latestPost(limit).find, latestPost(limit).options);

In essence, this function takes your query requirements (“give me the X latest posts”) and constructs something MongoDB can understand ({}, {sort: {createdAt: -1}, limit: X}). So we’ll call it a query constructor.

But our query constructor can do more: for example, you might want to limit the limit parameter to 100 to prevent users from asking for 10 million posts all at once and overloading your poor server. With the query constructor pattern, you only have to specify this limit in a single place:

// client & server
latestPost = function (limit) {

  if (limit > 100) {
    limit = 100;
  }

  return {
    find: {},
    sort: {sort: {createdAt: -1}, limit: limit}
  };
}

Query Views

So far we’ve only considered a scenario with a single way of filtering data. But in a real-world app, you’ll often want to retrieve documents using multiple sorts: creation date, last modified date, popularity, etc. Let’s call each of these views (this has nothing to do with “views” in the MVC sense, by the way).

First, each view will be defined by its own function:


views = {};

// client & server
views.latestPosts = function (terms) {
  return {
    find: {},
    sort: {sort: {createdAt: -1}, limit: terms.limit}
  };  
}

views.mostPopularPosts = function (terms) {
  return {
    find: {},
    sort: {sort: {score: -1}, limit: terms.limit}
  };  
}

For added flexibility, we’re passing a terms object as argument instead of just limit. This makes it easy to pass other options (terms.category, terms.date, etc.) in the future if we need to.

Also, notice how we removed the rate limiting? That’s because we’ll add it in a new, general query constructor function that makes use of our views:

// client & server

queryConstructor = function (terms) {

  var viewFunction = views[terms.viewName]
  var parameters = viewFunction(terms);

  if (parameters.limit > 100) {
    parameters.limit = 100;
  }

  return parameters;

}

And finally, here’s how we put our new query constructor function to work:

// server
Meteor.publish('posts', function(terms) {
  var parameters = queryConstructor(terms);
  return Posts.find(parameters.find, parameters.options);
});

// client
var terms = {
  viewName: Session.get('view'),
  limit: 20
}

Meteor.subscribe(terms);

var parameters = queryConstructor(terms);
var latestPosts = Posts.find(parameters.find, parameters.options);

In Practice

Once you’ve adopted this view/constructor pattern, creating new views becomes very easy. For example, here’s how Telescope sets up some of its views:

/**
 * New view
 */
Posts.views.add("new", function (terms) {
  return {
    options: {sort: {sticky: -1, postedAt: -1}}
  };
});

/**
 * Best view
 */
Posts.views.add("best", function (terms) {
  return {
    options: {sort: {sticky: -1, baseScore: -1}}
  };
});

/**
 * Pending view
 */
Posts.views.add("pending", function (terms) {
  return {
    find: {
      status: Posts.config.STATUS_PENDING
    },
    options: {sort: {createdAt: -1}},
    showFuture: true
  };
});

And I won’t go into too much details about Telescope’s central query constructor function right now, but here’s a sample of the tasks it handles:

  • Define a set of default finding/sorting options in case the current view doesn’t provide them.
  • Add a sort by _id to break ties.
  • Limit the number of posts.
  • Hide posts scheduled for a future date.

And since these are all part of the common query constructor function, these checks get automatically applied to all views.

Conclusion

This is a simple pattern, but hopefully it will help solve a common frustration with managing Meteor queries when publishing and subscribing. So let me know if you find it useful!