Search code examples
mongodbcollectionsmeteorsubscribeminimongo

Meteor: how to find a subdocument within a document (minimongo)


The short version

To find a document within a collection, it's

collection.findOne([...], [...])

How do you find a subdocument within a document?

The long version

I have a collection entry as below:

db.projects.find()

{
  title: 'company.com'
  company: 'A Company'
  companyID: Random.id()
  category: 'website'
  starred: false
  timeline: {
    ideas: {
      task: {
        name: 'task1'
        completed: true
        todos: [
          {todo: 'a todo', completed: false, todoID: Random.id()}
          {todo: 'a todo', completed: false, todoID: Random.id()}
          {todo: 'a todo', completed: false, todoID: Random.id()}
        ]
      }
      task: {
        name: 'task2'
        completed: false
        todos: [
          {todo: 'another todo', completed: false, todoID: Random.id()}
          {todo: 'another todo', completed: false, todoID: Random.id()}
          {todo: 'another todo', completed: false, todoID: Random.id()}
        ]
      }
    }
    development: {
    ...
    }
    production: {
    ...
    }
  }
}

(written in coffeescript)

The entry is in my Projects collection. It's published on the server:

server/publications.js

Meteor.publish('projects', function() {
  return Projects.find();
});

.. and subscribed to by the client:

client/projects.js

Meteor.subscribe('projects');

Simple. Works as expected.

Next, I use a Session variable to store a project when it is selected:

Session.set('selectedProject', this.id); 

and call it when required:

Session.get('selectedProject');

All fine.

Now I want to search the ideas entry of the selectedProject and find the first task that has completed: false.

After a few hours of reading, I think I'm close with the following:

 ({
   currentTask: function() {
     return Projects.findOne({
       _id: Session.get('selectedProject', {
         'timeline.ideas.task.completed': false
       })
     }, {
       fields: 'timeline.ideas.task'
     });
   }
 });

I guess ^ that might try to return one project and not one task?

it returns this error:

Exception in template helper: Error: Match error: Failed Match.OneOf or Match.Optional validation

in theory it should be something like

selectedProject.findOne(....)

do I look at using $elemMatch on the server? or am I missing something simple?


Solution

  • See this answer which uses aggregation.

    Meteor users: at the time of writing (version 1.0.4.1), aggregation on the client is not supported.

    I wanted to keep the code on the client, and also wanted reactivity, so here's my solution:

    db.projects

    // simplified structure
    
    {
          title: 'awebsite.com'
          company: 'a company'
          companyID: Random.id()
          category: 'website'
          starred: false
          tasks: [
              {
                completed: true
                name: 'task1'
                category: 'ideas'
                todos: [
                  {todo: 'something', completed: false, todoID: Random.id()}
                  {todo: 'something', completed: false, todoID: Random.id()}
                  {todo: 'something', completed: false, todoID: Random.id()}
                ]
              }
              {
                completed: false
                name: 'task2'
                category: 'ideas'
                todos: [
                  {todo: 'something', completed: false, todoID: Random.id()}
                  {todo: 'something', completed: false, todoID: Random.id()}
                  {todo: 'something', completed: false, todoID: Random.id()}
                ]
              }
            ]
        }
    

    ../projects.coffee

    Meteor.subscribe 'projects'
    Tasks = new (Mongo.Collection)(null)   //use (null) to create client-only collection
    
    Template.projects.rendered = ->
      results = Projects.findOne { title: 'awebsite.com' },
        fields: tasks: 1
    
      _.each results.tasks, (task) ->
        Tasks.insert (task)
    
    Template.projects.helpers
      currentTask: ->
        Tasks.findOne completed: false