Mongoose - Upserting Documents with Nested Models

I have a basic document with a 'checked_in' flag in my express app:

module.exports = Book= mongoose.model('Book', new Schema({
 name : String,
 checked_in : Boolean
},{ collection : 'Book' }));

I wanted to keep a log of when books are checked in and out so I came up with another schema:

var action = new Schema({
 checked_in: Boolean,     
});

module.exports = Activity = mongoose.model('Activity', new Schema({
 book_id: String,
 actions: [action]
},{ collection : 'Activity' }));

The 'book_id' should be the document id of a book and when I update a book I need to either create or update the activity log for that book with a new item inside of actions:

exports.update = function(req, res){  
    return Book.findById(req.params.id, function(err, book) {  
       var activity = new Activity({book_id: book.id});
       activity.actions.push({
           checked_in: req.body.checked_in,
       });

       Activity.update({ book_id: book.id}, activity.toObject(), { upsert: true }));

       book.checked_in = req.body.checked_in;
       return device.save(function(err) {
           return res.send(book);
       });
    });
};

The problem I am having is that nothing gets inserted into the Activity collection. If I use .save() then i just get lots of duplicates in the collection.

UPDATE

I've started re-working things with the advice given below but am still not having any luck with this. Here's what I have now:

module.exports = Activity = mongoose.model('Activity', new Schema({
  book_id: Schema.ObjectId,
  actions: [new Schema({
    checked_in: Boolean,
    last_user: String
  })]
},{ collection : 'Activity' }));

Here's the update code now:

exports.update = function(req, res){  
  // TODO: Check for undefined.
  return book.findById(req.params.id, function(err, book) {    
    if(!err) {
      // Update the book.
      book.checked_in = req.body.checked_in;
      book.last_user = req.body.last_user;    
      book.save();

      // If there's no associated activity for the book, create one. 
      // Otherwise update and push new activity to the actions array.
      Activity.findById(book._id, function (err, activity) {
        activity.actions.push({
          checked_in: req.body.checked_in,
          last_user: req.body.last_user
        })

        activity.save();      
      });
    }
  });
};

What I want to end up with is a document for each book with an array of check outs/ins that gets updated each time someone checks a book in or out. i.e:

{
    book_id: "5058c5ddeeb0a3aa253cf9d4",
    actions: [
        { checked_in: true, last_user: 'ralph' },
        { checked_in: true, last_user: 'gonzo' },
        { checked_in: true, last_user: 'animal' }
    ]
}

Eventually I will have a time stamp within each entry.

I see a few things that can be improved...

  • The book_id field in the Activity model should be Schema.ObjectId instead of a String. You will then be able to use populate if you wish.

  • You aren't doing any error checking in exports.update. If the user passes in an invalid id, you will want to check if book is undefined or not, as well as the common if (err) return next(err) (this requires your function params to be res, res, next).

  • When you create the activity in exports.update, you want to use book._id instead of book.id

  • All the return statements are not needed

  • The device variable is not declared anywhere, I'm not sure what you are trying to save... I think you meant book there.

You can then just .save() the activity instead of doing the Activity.update.

There are a couple problems:

  1. You're trying to find the book's activity doc using findById using the book's id instead of the activity's id.
  2. You're not handling the case where the book's activity doc doesn't exist yet.

Try this instead:

Activity.findOne({book_id: book._id}, function (err, activity) {
  if (!activity) {
    // No Activity doc for the book yet, create one.
    activity = new Activity({book_id: book._id});
  }
  activity.actions.push({
    checked_in: req.body.checked_in,
    last_user: req.body.last_user
  });

  activity.save();
});