8

I have two collections

  1. Offers (relevant fields: _id)
  2. ShareRelations (relevant fields: receiverId and offerId)

and I'd like to publish only Offers to the logged in user which have been shared to him.

Actually, I'm doing this by using a helper array (visibleOffers) which I fill by looping for each ShareRelations and use this array later on the Offers.find as $in selector.

I wonder if this might be the meteor way to do this, or if I could do with less and/or prettier code?

My actual code to publish the Offers is the following:

Meteor.publish('offersShared', function () {
  // check if the user is logged in
  if (this.userId) {
    // initialize helper array
    var visibleOffers = [];
    // initialize all shareRelations which the actual user is the receiver
    var shareRelations = ShareRelations.find({receiverId: this.userId});
    // check if such relations exist
    if (shareRelations.count()) {
      // loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
      shareRelations.forEach(function (shareRelation) {
        if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
          visibleOffers.push(shareRelation.offerId);
        }
      });
    }
    // return offers which contain the _id in the array visibleOffers
    return Offers.find({_id:  { $in: visibleOffers } });
  } else {
    // return no offers if the user is not logged in
    return Offers.find(null);
  }
});

Furthermore, the actual solution has the downside that if a new share relations is being created, the Offers collection on the client doesn't get updated with the newly visible offer instantly (read: page reload required. But I'm not sure if this is the case because of this publish method or because of some other code an this question is not primary because of this issue).

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • While this is a common pattern in relational databases, it's still a bit tricky to achieve this the Meteor way. You should have a look at this video : https://www.eventedmind.com/posts/meteor-how-to-publish-a-many-to-many-relationship – saimeunt Dec 24 '13 at 01:03
  • Maybe it's more simple to denormalize your data and add a `receiversId` array in the Offer collection? – mquandalle Dec 24 '13 at 15:53

3 Answers3

9

What you are looking for is a reactive join. You can accomplish this by directly using an observe in the publish function, or by using a library to do it for you. Meteor core is expected to have a join library at some point, but until then I'd recommend using publish-with-relations. Have a look at the docs, but I think the publish function you want looks something like this:

Meteor.publish('offersShared', function() {
  return Meteor.publishWithRelations({
    handle: this,
    collection: ShareRelations,
    filter: {receiverId: this.userId},
    mappings: [{collection: Offers, key: 'offerId'}]
  });
});

This should reactively publish all of the ShareRelations for the user, and all associated Offers. Hopefully publishing both won't be a problem.

PWR is a pretty legit package - several of us use it in production, and Tom Coleman contributes to it. The only thing I'll caution you about is that as of this writing, the current version in atmosphere (v0.1.5) has a bug which will result in a fairly serious memory leak. Until it gets bumped, see my blog post about how to run an updated local copy.

update 2/5/14:

The discover meteor blog has an excellent post on reactive joins which I highly recommend reading.

Community
  • 1
  • 1
David Weldon
  • 63,632
  • 11
  • 148
  • 146
0

The way to do this is along the lines of this Question using observeChanges(). Still trying to figure out how to get it all working for my example, see Meteor, One to Many Relationship & add field only to client side collection in Publish?

Community
  • 1
  • 1
Giant Elk
  • 5,375
  • 9
  • 43
  • 56
0

You can use the reactive-publish package (I am one of authors):

Meteor.publish('offersShared', function () {
  // check if the user is logged in
  if (this.userId) {
    this.autorun(function (computation) {
      // initialize helper array
      var visibleOffers = [];
      // initialize all shareRelations which the actual user is the receiver
      var shareRelations = ShareRelations.find({receiverId: this.userId}, {fields: {offerId: 1}});
      // loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
      shareRelations.forEach(function (shareRelation) {
        if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
          visibleOffers.push(shareRelation.offerId);
        }
      });
      // return offers which contain the _id in the array visibleOffers
      return Offers.find({_id:  { $in: visibleOffers } });
    });
  } else {
    // return no offers if the user is not logged in
    return Offers.find(null);
  }
});

You can simply wrap your existing non-reactive code into an autorun and it will start to work. Just be careful to be precise which fields you query on because if you query on all fields then autorun will be rerun on any field change of ShareRelations, not just offerId.

Mitar
  • 6,756
  • 5
  • 54
  • 86