Search code examples
mongodbmeteormongodb-querymeteor-publications

Meteor / Mongo find a record and prune a subarray of child id's based on a child field?


I have a collection like this in Meteor:

TagsToArticles = {
  tag: "Tag1",
  articles: [ article1Id, article2Id, article3Id ]
}

The articles collections has the following schema

Articles = {
  permission: "private"
  ...
}

The tags are essentially indexed searchterms.

Each article has a permission set to "private", "group" or "public".

Right now, I'm publishing the tags like this:

Meteor.publish("allTags", function() {
  return TagsToARticles.find({});
}

And then in the client, I'm filtering the list of articles and showing only those that are public or those that are private but created by the current user.

Ideally, however, for security purposes, I'd like to do the filtering on the server-side, within the publish function itself, to prevent clients from having access to the article Ids of private articles. I do prevent the actual article objects from being accessed unless the client has the proper permissions, but I'd like to go one step further and remove the IDs altogether from the results.

So what i'm looking for is essentially a query that allows me the following pseudocode:

TagsToArticles.find({ articles.foreach(articleId) {
  if (Articles.findOne(articleId).permission == 'public') ||
     (Articles.findOne(articleId).ownerId == Meteor.userId())
     include articleId
  }

I originally thought about doing this with a function exactly like above (basically fetching all the records and then going through each one and pruning the arrays manually, then returning the updated set of records), but my understanding is that I would then lose Meteor's reactivity and the record set won't update if the underlying data changes.

In the absence of a single find() query that does the job, if there's a way to do an additional pass with a function and still return a reactive data set, I'd be fine with that solution too.

Since this is a denormalized collection anyway (the tags are also located within the articles document), I guess I could also denormalize further and include not just the article ID but the ownerId and permission. But I'm still not sure how to test individual array elements, plus if possible, I'd like to minimize the amount of denormalization I need to do...


Solution

  • You should consider using the composite-publish package.

    The publish code would look something like this if I understand your schema correctly:

    Meteor.publishComposite('articles', function() {
      return {
        find: function() {
          return Articles.find({ $or: [ { permission: 'public' }, { ownerId: this.userId } ] });
        },
        children: [{
          find: function(article) {
            return TagsToArticles.find( { articles: article.articleId } );
          }
        }]
      }
    });
    

    The package does a reactive join, which is actually pretty expensive. So keep that in mind and see if this is still a good choice for you.

    p.s. I remember that you cannot use Meteor.userId() in publish, that's why I used this.userId