Search code examples
javascriptmongodbmeteormeteor-publications

How to setup a mongo projection AFTER a server side transform function in Meteor.js?


I need to limit the number of fields sent to the client from a publish function after applying a transform that requires access to the original doc.

I'm basically trying to avoid sending potentially huge arrays down to the client, and run a bunch of checks to return a nice neat object to work with.

Heres the function I've got now - it works, just the not the way I'd like, basically limiting the fields given to observe function. is there a way to add the projection after the observe / transform.

Meteor.publish('network', function() {

  var self = this;

  // get the user values initially
  var user = Meteor.users.findOne(self.userId);
  var followingUsers = user.following ? user.following.users || [] : [];
  var followingChannels = user.following ? user.following.channels || [] : [];

  var transformMedia = function(doc) {
    // get the user each time to keep this publication reactive
    votesUp = doc.votes ? doc.votes.up || [] : [];
    votesDown = doc.votes ? doc.votes.down || [] : [];
    favourites = doc.votes ? doc.votes.favourites || [] : [];

    doc.userActions = {
      votedUp: _.contains(votesUp, doc._id) ? 1 : 0,
      votedDown: _.contains(votesDown, doc._id) ? 1 : 0,
      isFavourite: _.contains(favourites, doc._id) ? 1 : 0,
      played: _.contains(doc.played, self.userId) ? 1 : 0,
    };

    return doc;
  };

  var networkQuery = Media.find({
    $and: [
    {
        $and: [
          {processedAt: { $exists: true} },
          {processedStatus: 'successful'},
          {publishStatus: 'published'}
        ]
      },
      {
        // if created by this user, user they follow or channels they subscribe to
        $or: [
          {createdBy: self.userId },
          {createdBy: { $in: followingUsers} },
          {channels: { $in: followingChannels} },
        ]
      }

      // TODO : add not banned or trashed once implemented
    ]
  }, mediaModifiers).observe({
    added: function(doc) {
      self.added('media', doc._id, transformMedia(doc));
    },
    changed: function(doc, oldDoc) {
      self.changed('media', doc._id, transformMedia(doc));
    },
    removed: function(doc) {
      self.removed('media', doc._id, transformMedia(doc));
    },
  });

  self.onStop(function() {
    networkQuery.stop();
  });

  self.ready();

});

Solution

  • I had a similar issue once. I dealt with it using cursor.observe()+ a custom function (as you did) and I just added a _.pick() to filter the unnecessary fields. Have a look at this publication code for an example (the white list docToPublish part especially):

    var self = this;
    
    // Modify the document we are sending to the client.
    function filter(doc) {
      var length = doc.item.length;
    
      // White list the fields you want to publish.
      var docToPublish = _.pick(doc, [
          'someOtherField'
      ]);
    
      // Add your custom fields.
      docToPublish.itemLength = length;
    
      return docToPublish;                        
    }
    
    var handle = myCollection.find({}, {fields: {item:1, someOtherField:1}})
                // Use observe since it gives us the the old and new document when something is changing. 
                // If this becomes a performance issue then consider using observeChanges, 
                // but its usually a lot simpler to use observe in cases like this.
                .observe({
                    added: function(doc) {
                        self.added("myCollection", doc._id, filter(doc));
                    },
                    changed: function(newDocument, oldDocument)
                        // When the item count is changing, send update to client.
                        if (newDocument.item.length !== oldDocument.item.length)
                            self.changed("myCollection", newDocument._id, filter(newDocument));
                    },
                    removed: function(doc) {
                        self.removed("myCollection", doc._id);                    
                    });
    
    self.ready();
    
    self.onStop(function () {
      handle.stop();
    });
    

    This code is borrowed from @datacarl answer to my topic mentioned above.

    Note that if you scale up to several servers, the cons of this approach is that each server will have to run the cursor.observe() function.

    You also forgot to make your publication ready and dispose of your observers at the end of your publication (it might be because you didn't paste all the pub). It would look like this :

    self.ready();
    self.onStop(function () {
      networkQuery.stop();
    });