2

What I want to accomplish on http://crowducate.me:

  • Display the usernames of the course authors (i.e. "owner" of a document).

Current Code:

Meteor.publish 'popularCourses', ->
# find all courses
  courses = Course.find({}, {sort: {createdAt: -1}}).fetch()
  for course in courses
# find each User by course owner
    owner = Meteor.users.findOne({_id: course.owner})
# overwrite the ownerId with the desired username
    course.owner = owner.username
  return courses

If I turn autopublish on, it works. The image shows the current status (autopublish off). As seen in the image, the author's name is only rendered if the current user is the same as the author.

enter image description here

--

A friend suggested the following: https://gist.github.com/wiesson/1fd93d77ed9df353b7ab

"The basic idea was to attach the username to the course before providing the data with the publish method. However, as described in Meteor MongoDB find / fetch issues, the publish method should return a curser and not an array of objects.”

Any ideas how to solve that? Putting the owner usernames in an array? If so, how?

P.S.: Sourecode can be found here (currently, has more commits than the deployed version): https://github.com/Crowducate/crowducate.me

Thanks a lot.

Community
  • 1
  • 1
Amir Rahbaran
  • 2,380
  • 2
  • 21
  • 28

2 Answers2

3

There are a couple of ways you can accomplish this join. A few notes before before we begin:

  • As I explained in the answer to this question, sorting in the publish function has no affect on the order of documents on the client.

  • Using the plural form in a collection name is the accepted standard. Course just looks odd when the collection contains courses.

  • This question is fundamentally about joins, so I'd recommend reading Reactive Joins In Meteor.

Server Transform

The literal answer to your question is to transform the documents on the server like so:

Meteor.publish 'popularCourses', ->
  transform = (fields) ->
    if fields.owner
      username = Meteor.users.findOne(fields.owner)?.username
      fields.owner = username
    fields

  handle = Course.find().observeChanges
    added: (id, fields) =>
      @added 'course', id, transform fields

    changed: (id, fields) =>
      @changed 'course', id, transform fields

    removed: (id) =>
      @removed 'course', id

  @ready()

  @onStop ->
    handle.stop()

Advantages

  • All of the work is done on the server, so the client can just use owner as if it was a username.

Disadvantages

  • Using observeChanges is probably more computational work than a simple join deserves.

  • If you publish courses somewhere else, it's entirely likely that owner will be overwritten when the documents are merged on the client. This can be countered by appending a field like ownerUsername but that would also require a more expensive observe.

  • This isn't helpful if you actually need the owner id somewhere on the client.

  • It isn't reactive if the username changes (probably rare but figured I'd point that out).

Non-Reactive Publish + Client Join

You could implement the publish like this:

CoffeeScript

Meteor.publish 'popularCourses', ->
  courseCursor = Course.find()
  userIds = courseCursor.map (c) -> c.owner
  userCursor = Meteor.users.find {_id: $in: userIds}, {fields: username: 1}
  [courseCursor, userCursor]

JavaScript

Meteor.publish('popularCourses', function() {
  var courseCursor = Course.find();
  var userIds = courseCursor.map(function(c) {return c.owner;});
  var userCursor = Meteor.users.find(
    {_id: {$in: userIds}}, 
    {fields: {username: 1}
  });
  return [courseCursor, userCursor];
});

Note that I'm being careful to only publish the username and _id from userCursor (you don't want to publish the hashed password and session data by accident). Then you can join the two collections on the client like this:

Template.myTemplate.helpers
  courses: ->
    Course.find().map (c) ->
      c.owner = Meteor.users.findOne(c.owner)?.username
      c

Advantages

  • Computationally light-weight and simple publish function.

  • Reactive if the username changes.

Disadvantages

  • Not reactive if the owner changes.

  • You'll need to do the join on the client. An interesting alternative is to use something like Collection Helpers.

Finally, I'll point out that you can use a package to do a fully reactive join. However, unless the owner (or owner's username) is changing a lot then this is probably overkill.

Community
  • 1
  • 1
David Weldon
  • 63,632
  • 11
  • 148
  • 146
  • 1
    Thanks for the detailed answer! I'm not that familiar with Coffeescript, however I thought the Publish method have to return a cursor. If I use your code, it does not work, so give me a short hint ;) – wiesson Sep 15 '14 at 07:49
  • 2
    Sorry about that - I usually try to give answers in the same dialect as the question. Note: 1) coffeescript has implicit returns so I'm returning an array from the publish function, 2) publish functions can return a cursor or an array of cursors. The code I gave is nearly identical to the first example javascript [here](https://www.discovermeteor.com/blog/reactive-joins-in-meteor/), so you can use that as a guide. – David Weldon Sep 15 '14 at 17:39
  • Amir is more familiar with coffeescript than me, however we are both trying to understand the problem. I don't understand how you are returning a cursor (or a list or cursers). Have you seen the router? https://github.com/Crowducate/crowducate.me/blob/master/client/router/routes.coffee#L203-L207 - is this with your adjustments still correct? – wiesson Sep 16 '14 at 16:41
  • 2
    I updated the answer with the JS version of the publish function. Does that help answer the question? It really doesn't have anything to do with your router since we are still publishing courses... we just also happen to be publishing some users as well. The `waitOn` should still work. I'm not sure what's going on in your `data` section but I'm not familiar with `minimongoid`. – David Weldon Sep 16 '14 at 16:58
  • 1
    @DavidWeldon: This has helped a lot. Thanks a lot for your time. Highly appreciated. – Amir Rahbaran Sep 17 '14 at 08:54
1

A simple solution would be to just publish both popularCourses and owners and add the owner to each course on the client (with the exact same code you have written on the publication).

Tomas Romero
  • 8,418
  • 11
  • 50
  • 72