How to "change" published field sets in Meteor/mongo

125 views Asked by At

Does anyone know how to "change" field sets in a publication (Meteor.publish or Meteor.publishComposite) based on the certain characteristics/field values of the record with respect to the logged on user?

I was using a publishComposite, with cursors at the same level, like so:

Meteor.publishComposite("games", [
  {
    find: userRecord,
    children: [
      {
        find: gamesWeArePlaying
      }
    ]
  },
  {
    find: userRecord,
    children: [
      {
        find: gamesWeOwn
      }
    ]
  },
  {
    find: userRecord,
    children: [
      {
        find: examineWithAnalysis
      }
    ]
  },
  {
    find: userRecord,
    children: [
      {
        find: examineWithoutAnalysis
      }
    ]
  },
  {
    find: userRecord,
    children: [
      {
        find: allGames
      }
    ]
  }
]);

Each find function returns a cursor with qualifying records, and the field set allowed for that user in that state. Just be aware that in each of the children finds, it will find a set of unique records, but the fields it returns is specific to that cursor. The most common are transitions from "gamesWeArePlaying" to "examineWithAnalysis", and in the case of teacher/student, "examineWithAnalsyis" to "examineWithoutAnalysis."

But what is happening is that when a game (i.e. a single record) transitions from one cursor to another, the underlying framework fails to send correct "changed" fields to the client. The minimongo record(s) get all out of whack, and do not even match what's actually in the servers database.

So, my question is: What is the best way to return, reactively, a set of records, where every single record has a specific field set returned, based on characteristics of said record?

2

There are 2 answers

0
Danail Gabenski On

I read through your github code and it seems like your true intention here is to publish all games that this user should have "access" to and you want the user to have different permissions for each of the games. This is more of a mongodb solution, you want to have a normal publish(not composite, they are slow and don't work well) where you first get the user ID. Then just query for all the games (as you already are as a composite) like this

{
  $or : [
    { isolation_group : user.isolation_group }, // part of the user's isolation group
    { owner           : user._id }              // games we own
  ]
}

This should retrieve all the games the user should be privy to based on their isolation group or their ownership. I would suggest adding the isolation_group even to games you own in order to simplify the query - but that's optional. After you retrieve all these games you'll have them in an array, this is where a library like lodash comes to our help - you can do a series of _.map() calls like this

let gamesWeArePlaying = _.compact(_.map(ArrayOfAllGames, (game, key) => {
  if (game.status !== "playing" || 
        (game.white.id !== user._id && game.black.id !== user._id)) {
    return '';
  }

  return _.pick(game, [
    "black", "clocks", "fen", "lag", "observers", "pending",
    "rated", "rating_type", "startTime", "status", "tomove",
    "variations", "white", "wild"
  ]);
}));

Here is where it becomes a bit tricker, if you publish them as you mentioned with this.added/changed/removed to the same client side collection you are bound to get the same game, but you want to show it with specific fields based off of the results of the multiple _.map() calls. If you add the same game multiple times Meteor will publish the same game multiple times with the different permissions which is not what you want. So modify that map call to actually check for all permissions at once (i.e. gamesWeArePlaying, gamesWeOwn, examineWithAnalysis, ...) and change the _.pick() list of fields based on that. This will give you a unified array of games with proper fields to publish that will get updated reactively.

1
David Logan On

OK guys, I figured it out, and it was a stupid programmer error on my part. First, let me state that publishComposite, and changing fields, (alleged performance issues aside) does in fact work! Something like this does work:

publishComposite("publication", {
   find() {return collection1.find()},
   children: [
      {find(c1_record) {return collection2.find({s1},{fields: {f1: 1}})}}
      {find(c1_record) {return collection2.find({s2},{fields: {f1: 1, f2: 1}})}}
      {find(c1_record) {return collection2.find({s3},{fields: {f2: 1}})}}
   ]
});

No issues with this with Mongo, nor Meteor, nor DDP, nor the client, nor minimongo. It works great.

My problem is that you have to be REALLY REALLY careful when you use publishComposite when higher level finds are not meant for publication, but only for sub-qualifying records. Let's use my example. Above, I was publishing game records, and publishing certain fields based on certain states (if you were playing, if you could see computer analysis, etc.)

But, elsewhere, I have to publish chat records. In order to correctly publish chat records, I had to figure out what games a user was playing or observing. Whether they were playing or could see computer analysis was unimportant. I simply needed to know which games they were playing. So I did this:

publishComposite("chat", {
find() {
  return Meteor.users.find({_id:...})
},
children[
  {
    find() {return gamescollection.find({..all games user is involved with...})}, 
    children: [{
      find(){
        chat.find({...chats for game...})}
    }]
  }
]
});

See the problem?

The top level find publishes the user record, all fields in all its glory (possible problem #1!)

The second level find publishes the game record, all fields in all its glory. And that, my friends, was the problem. I simply did not put that together because I was not publishing the user nor the game on purpose, it was only for providing the requisite data for publishing the chat records at the the lowest level.

I changed this latter publication to return only the ID of the game records the user is involved with (i.e. gamescollection.find({...},{fields: {_id: 1}}), and just like that, everything worked.