6

In my application, sections is a collection linked to courses by a property called course.sectionIds. The initial load works fine, but I am running into a non-reactive join problem when adding a section in the admin panel.

Here's the route:

@route 'adminCourse',
    path: 'admin/course/:course'
    waitOn: -> Meteor.subscribe 'course', @params.course
    data: -> Course.first()

And the sections are included in the course publication:

Meteor.publish 'course', ( courseId ) ->
    return null if not this.userId

    # [some permission checks]

    courses = Course.find courseId
    sections = Section.find _id: $in: _.flatten courses.map ( course ) -> course.sectionIds

    [ courses, sections ]

I know about reactive joins, but I can't really use approach #1 or #4 (overpublishing and joining on the client), as there are permission checks involved (you should only be able to see the sections of your own courses). Also, I know when the data changes, so it doesn't really have to be reactive.

I just want to let Meteor know to reload the data, when the user submits the form for adding a new section (I am currently working around this by doing a window.location.reload() after a section has been added). Is there a way to do that with Meteor?

Rijk
  • 11,032
  • 3
  • 30
  • 45

3 Answers3

8

Turns out this was really simple, and I feel a little stupid now :)

You can reload the subscription by simply calling Meteor.subscribe() again. I found this out while messing with a different solution, using iron router to navigate to a different URL making it reload the subscription.

So, in my submit listener, instead of doing window.reload(), you can simply do the following:

Template.sectionForm.events
    'submit form': ( e ) ->
        e.preventDefault()
        data = SimpleForm.processForm( event.target )

        section = Section.create( data )
        this.course.push( sectionIds: section._id )

        # Reload the subscription to pull in the new section
        params = Router.current().params
        Meteor.subscribe 'course', params.producer, params.course

And it will pull in the new data. Yay!

Rijk
  • 11,032
  • 3
  • 30
  • 45
  • Strictly speaking, this is the answer to my question. But Mitar's suggestion of putting the main item's ID in all subitems (pre-joining so to speak) was way better and I actually ended up using that approach. – Rijk Oct 14 '14 at 11:15
7

For who's interested, I fixed it by adding observeChanges in my publish function:

Meteor.publish 'course', ( courseId ) ->
    return null if not this.userId

    # [some permission checks]

    courses = Course.find courseId
    sectionIds = _.flatten courses.map ( course ) -> course.sectionIds

    handle = courses.observeChanges
        changed: ( id, fields ) =>
            if _.has fields, 'sectionIds'
                addedIds = _.difference fields.sectionIds, sectionIds
                removedIds = _.difference sectionIds, fields.sectionIds
                sectionIds = fields.sectionIds

                _.each addedIds, ( id ) => @added 'sections', id, Section.first id
                _.each removedIds, ( id ) => @removed 'sections', id

    @onStop -> handle.stop()

    sections = Section.find _id: $in: sectionIds

    [ courses, sections ]

The observer checks for changes in the sectionIds property, and when it happens calls either the added or removed methods on the subscription. This makes the join reactive; when adding IDs to the courses.sectionIds property the new section documents are automatically pushed to the client now.

Rijk
  • 11,032
  • 3
  • 30
  • 45
  • 2
    Neat. I still feel like this is a lot of hard work for a functionality that should be in core. I hope [it makes it into 1.0](https://trello.com/c/BGvIwkEa/48-easy-joins-in-subscriptions). – Benjamin Crouzier Sep 29 '14 at 14:04
  • 3
    I know... Meteor is great until you run into something that it doesn't do ;) – Rijk Sep 29 '14 at 14:52
  • @DanDascalescu you sure that link is correct? Doesn't seem to be pointing to any initiative. – Rijk Jan 01 '15 at 17:11
  • @pinouchon: if you want this functionality in core, you may want to join the [fork Meteor initiative](https://github.com/MeteorCommunity/discussions/issues/44). – Dan Dascalescu Jan 03 '15 at 03:10
3

You could just meteor-related package:

Meteor.publish 'course', (courseId) ->
  # verify that courseId is really an ID and not {} or something else

  return unless @userId

  @related (course) ->
    # [some permission checks]

    Section.find
      _id:
        $in: course.sectionIds
  ,
    Course.find courseId

I assume here that courseId matches only one course. So meteor-related works only with one-to-many relations. In your code above you did some many-to-many things.

One other way is that in all those documents (questions, options, steps, etc.) you have a main ID, so course ID. And then you just filter all those documents based on course ID and publish that to the client, and then you on the client display this in whatever hierarchy you want. So instead of trying to compute relations at read time, you compute relations at write time and just store the ID of that relation into all documents you are interested.

Disclaimer: I am the author of this package.

Mitar
  • 6,756
  • 5
  • 54
  • 86
  • You could also look into [PeerDB](https://github.com/peerlibrary/meteor-peerdb) package. – Mitar Sep 30 '14 at 00:23
  • Thanks Mitar, I'll definitely check it out. It's only one to many relations: course 1—n section 1—n step. I've applied my 'homebrew' solution to all of these, and it works for single links (making new sections and steps appear automatically) but it's not yet fully reactive: when a new step is added to a new section it does NOT appear (I guess because the new section is not in the result set being observed for changes). Can you nest relations in `meteor-related`? – Rijk Sep 30 '14 at 06:18
  • Maybe, I have not tested nested relations. I think you should rethink the approach. Even if you make it work, it means that number of queries you make by creating one subscribe is huge, if you have n sections, then you will be making n queries for steps, unless there is some additional joining magic and optimizations on queries. But then the question is: why are you using MongoDB if you want to use joins. Or: why are you using such schema in MongoDB. I think you are doing to relational schema. Try to simply use subdocuments and arrays. The whole idea of MongoDB is to make it do one query. – Mitar Sep 30 '14 at 08:23
  • I know, that's how I started out but it totally blew up in my face :/ Had a _really_ hard time updating the steps and sections (see http://stackoverflow.com/questions/25548548/how-to-query-and-update-nested-arrays). Then I looked at the crowducate.me source code and copied their approach (which is seperate collections for each object type — indeed much like a RDB). If I'm missing something, please let me know :) – Rijk Sep 30 '14 at 12:54
  • I don't know what blew up in your face. But for me having arrays of subdocuments seems quite easy approach. – Mitar Oct 01 '14 at 09:40
  • As you can read in the question I linked in my comment, arrays of subdocuments worked ok until I wanted to update one of them. For instance: set the title on step with ID "x" in section with ID "y". I could not figure out how to do that, as you can only use the `$` operator once. And since the question still has no answers, apparently neither can the rest of SO.. – Rijk Oct 01 '14 at 11:55
  • Yes, but you can mix this two solutions together: Have sections as objects and have an array of steps, so only one level of documents. And then you use meteor-related as a combination between courses and sections. – Mitar Oct 01 '14 at 18:10
  • But then I also have questions in the steps... And options in the questions :) – Rijk Oct 02 '14 at 12:21
  • OK, one other way is that in all those documents (questions, options, steps, etc.) you have a main ID, so course ID. And then you just filter all those documents based on course ID and publish that to the client, and then you on the client display this in whatever hierarchy you want. So instead of trying to compute relations at read time, you compute relations at write time and just store the ID of that relation into all documents you are interested. – Mitar Oct 02 '14 at 19:15
  • Very smart, I like it. Could even make that reactive relatively easily I think. I'll definitely try it out, thanks for the insight! – Rijk Oct 02 '14 at 20:21
  • 2
    Mitar you are my hero. It totally works, *and* is reactive. Great suggestion, thanks again! – Rijk Oct 13 '14 at 19:43