Publications and subscriptions tend to be one of the biggest stumbling blocks for Meteor newcomers.

And it’s easy to see why. Let’s say you have a Widgets collection in your database containing 10 widgets, and you want to display a list of these widgets on the client.

First, on the server you need to publish these widgets:

Meteor.publish("widgets", function () {
  return Widgets.find();
});

Then, on the client you need to subscribe to the publication you just created:

Meteor.subscribe("widgets");

And only then can you display your widget list to your users:

Widgets.find();

This complexity is probably why the autopublish package is still included by default in every newly created Meteor app, even though it only pushes the problem further down the line without really solving it.

Getting Rid of publish and subscribe

After explaining publish and subscribe in our book, on this blog, and in our podcast, I started wondering if there couldn’t be an easier way. And this is what led me to creating the SmartQuery package.

The idea behind SmartQuery is that calling Widgets.find() should be enough to ask for the data you need; and that it’s then up to the server to decide if you can or not access that data.

In other words, you can think of SmartQuery as autopublish, but with an added security layer similar in principle to Allow & Deny.

See It In Action

You can see SmartQuery in action in this simple demo, and check out the demo code here on GitHub.

See how the app’s code doesn’t contain a single Meteor.publish or Meteor.subscribe call?

Using SmartQuery

Here’s how it works in practice. On the client, find a helper that returns a cursor, such as:

Template.posts.helpers({
  posts: function () {
    var cursor = Posts.find({}, {limit: Session.get("postsLimit")});
    return cursor;
  }
}

Now just wrap that cursor in SmartQuery.create():

Template.posts.helpers({
  posts: function () {
    var cursor = Posts.find({}, {limit: Session.get("postsLimit")});
    return SmartQuery.create("posts", cursor);
  }
}

You’ll notice I’m also naming my new SmartQuery with a unique identifier ("posts") to make it possible for the app to keep track of the subscription.

And that’s it! In the background, SmartQuery will automatically create the appropriate publication and subscriptions.

Note that since we’re returning a SmartQuery object and not a cursor, we can’t do {{#each posts}} in our templates like we usually would. Instead, we just do {{#each posts.cursor}}.

Of course you could also return SmartQuery.create("posts", cursor).cursor to save you the trouble, but as we’ll see SmartQuery objects have a couple extra useful properties.

SmartQuery Objects

Besides cursor, objects returned by SmartQuery.create() also have the following properties:

  • subscription: the subscription created for this SmartQuery.
  • isReady(): (function) whether the subscription is ready (Boolean).
  • count(): (function) returns a count of the cursor.
  • totalCount(): (function) returns the total count for the cursor on the server (without a limit).
  • hasMore(): (function) whether there are more documents on the server (Boolean).

Especially useful is the totalCount() function, which returns how may total documents are available on the server for this specific Mongo query. This comes in very handy for easier pagination!

The Security Layer

If we just stopped here, what we’d have is a slightly improved autopublish. But that’s where the security layer comes in.

By calling SmartQuery.addRule() on the server, you can define rules that govern which data should or shouldn’t be published.

addRule() takes two arguments: the collection to which the rule will apply, and a rule object that can contain one or both of these properties:

  • filter(document): a function that, if provided, must return true for every single document in the cursor. Inside the filter function, you can call this.userId to access the current user’s _id.
  • fields(): a function that, if provided, must return an array of publishable fields. If no fields function is provided, all fields will be published.

Here’s what it looks like in practice:

SmartQuery.addRule(Posts, {
  filter: function (document) {
    return document.published === true; // only publish documents where published is true
  },
  fields: function () {
    return ["_id", "title", "body"]; // only publish these three fields
  }
});

How It Works

So how does this all work? It’s actually pretty simple: on startup, SmartQuery will loop through all your collections, and create a new publication on the server (named SmartQuery_CollectionName) for any collection for which a rule has been defined.

When you call SmartQuery.create() on a cursor, it then finds out the collection targeted by the cursor, and creates a new subscription to the corresponding publication.

This does mean you get one subscription per cursor, which might not be the most efficient way of doing things. But further testing would be required to find out if the performance impact is noticeable or not.

Limitations

SmartQuery is something I built as a proof of concept, without really considering its performance implications or whether it would work in larger, more complex apps.

So as of right now, I would probably still label it experimental and warn against using it for serious, production-grade apps.

Still, I think there might be something here, and I would love to get more users and feedback to keep improving the package.

After all, technologies like Firebase already use a similar rule-based system to control data access. And GraphQL also features a model where the client asks for the data it needs directly.

So there’s no reason why Meteor couldn’t explore this direction as well. In any case, I’d be curious to know what you all think!