0

Currently What I want to do is, once the user is logged in he could go to any user's url and follow them. Im getting this problem

TypeError: Object { _id: 54bd6b90b7d4cc8c10b40cbd,
  name: 'Johnny',
  username: 'batman',
  __v: 0,
  following: [],
  followers: [] } has no method 'save'

schema.js

var UserSchema = new Schema({
    name: String,
    username: { type: String, required: true, index: { unique: true }},
    password: { type: String, required: true, select: false },
    level: String,
    followers: [{ type: Schema.Types.ObjectId, ref: 'User'}],
    following: [{ type: Schema.Types.ObjectId, ref: 'User'}]
});

module.exports = mongoose.model('User', UserSchema);

api.js

        // this is where I'm Stuck ( Should I use put or post? for best practices)

        .post(function(req, res) {

            // find a current user that has logged in
            User.find({ _id: req.decoded.id }, function(err, user) {

                // simply follow the user by matching the id
                user.following = req.params.user_id;

                // save to the database ( The problem )
                user.save(function(err) {
                    if(err) res.send(err);

                    res.json({ message: "Successfully followed!"})
                })
            });
        })

The problem is in .post since I couldn't save. What should I do to make it save to the database?

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Naufal Yahaya
  • 79
  • 1
  • 1
  • 6

2 Answers2

1

Your problem in your code is that .find() does not return a single mongoose document but rather an array of documents even if that array contains only a single item. You can correct this by calling .findOne() or even .findById which allows a much shorter form.

As mentioned earlier, this still has problems as there is no guarantee that the document on the server has not already been changed before you issue the .save() after making modifications. For this reason MongoDB provides an.update()` method which will only make additional changes to the document via specified operators.

To add a new follower, you need to $push the element onto the array:

.post(function(req, res) {

  User.update(
    { 
        "_id": req.decoded.id, 
        "following": { "$ne": req.params.user_id }
    }, 
    { "$push": { "following": req.params.user_id }}, 
    function(err, user) {
      if (err) return res.send(err);

      res.json({ message: "Successfully followed!"})
    }
  );
})  

I actually check to see if that element is not present and then only update where that is the case. There is an $addToSet operator that does this, but I would also suggest modifying your schema to include a "followingCount": and similar fields. These are useful for queries and it is not a simple matter to return the "length" of an array without reading the entire document:

var UserSchema = new Schema({
    name: String,
    username: { type: String, required: true, index: { unique: true }},
    password: { type: String, required: true, select: false },
    level: String,
    followers: [{ type: Schema.Types.ObjectId, ref: 'User'}],
    following: [{ type: Schema.Types.ObjectId, ref: 'User'}],
    folloersCount: Number,
    followingCount: Number
});

// And in later code

  User.update(
    { 
        "_id": req.decoded.id, 
        "following": { "$ne": req.params.user_id }
    }, 
    { 
        "$push": { "following": req.params.user_id }},
        "$inc": { "followingCount": 1 }
    },
    function(err, user) {
      if (err) return res.send(err);

      res.json({ message: "Successfully followed!"})
    }
  );
})  

That increases the counter value by using the $inc operator. To do the reverse and remove a follower use $pull

  User.update(
    { 
        "_id": req.decoded.id, 
        "following": req.params.user_id 
    }, 
    { 
        "$pull": { "following": req.params.user_id }},
        "$inc": { "followingCount": -1 }
    },
    function(err, user) {
      if (err) return res.send(err);

      res.json({ message: "Successfully Un-followed :("})
    }
  );
})  

That does what $addToSet cannot and will not touch the documents when the query conditions are not met.

If you want the modified document in the response, use .findOneAndUpdate() instead.

So just because you are using mongoose, don't forget that you are still basically using MongoDB. It is better to use the database operators to modify documents rather than do those modifications in code.

Read up on the query and update operators in the documentation. In fact read up on all of them as it is worthwhile learning.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
  • This is a spectacular answer! I learn a lot from your solution. I just have one question though , What is $ne? and did you purposely left **following: {"$ne": req.params.userId}** ? or is it supposed to be **user_id**? Thank you again – Naufal Yahaya Jan 23 '15 at 03:09
  • @NaufalYahaya Fat fingers and eager copy and paste might not have made that clear. [`$ne`](http://docs.mongodb.org/manual/reference/operator/query/ne/) means "not equal to", so you use that parameter from your request ( once typo is corrected ) to make sure that that "follower" is not already in the array and you don't duplicate this, or increase the count when they are already following. Edited my mistake and added more links for you to read. Benefit of a high rep is that I can add lots of links :) – Neil Lunn Jan 23 '15 at 03:17
  • haha Thank you again for giving a great advice! I like "Fat fingers and eager copy and paste might not have made that clear" part In fact I'm doing it again :). Anyway thank you so much for all the links and advice! it means a lot for a newbie programmer like me. – Naufal Yahaya Jan 23 '15 at 03:21
  • Hey Neil Lunn, I need your help, I have a new question http://stackoverflow.com/questions/28474558/updating-2-mongoose-schemas-in-an-api-call , I cant no longer ask questions on this account. Its based on your code that you were helping me – Naufal Yahaya Feb 13 '15 at 04:43
0

You should use the update method, as this will perform an atomic operation and is not prone to any concurrency issues. This is because it will perform the find query and save at the same time.

.post(function(req, res) {

  User.update({ _id: req.decoded.id }, { $push: {following: req.params.user_id }}, function(err, user) {
    if (err) return res.send(err);

    res.json({ message: "Successfully followed!"})
  });
})  
Yuri Zarubin
  • 11,439
  • 4
  • 30
  • 33
  • 1
    I can elaborate on my own comments and call them an answer as well. This is incorrect as it would overwrite the complete contents of the existing document. – Neil Lunn Jan 23 '15 at 01:35
  • @yzarubin should i follow this method since NeilLunn said this is incorrect? – Naufal Yahaya Jan 23 '15 at 01:49
  • @yzarubin it is working but it overwrite the existing one. What Neil Lunn said is correct – Naufal Yahaya Jan 23 '15 at 01:50
  • @NeilLunn could you please elaborate your answer since I'm confuse which is the best practices. – Naufal Yahaya Jan 23 '15 at 01:51
  • I forgot to add $set, sorry about that, I've modified my answer. @Neil Lunn I didn't even read your message, this is a pretty standard answer. – Yuri Zarubin Jan 23 '15 at 02:11
  • @yzarubin i received an error when implementing the edited version, something like "Unexpected token {", I believe it doesnt accept $set variable – Naufal Yahaya Jan 23 '15 at 02:24
  • @yzarubin seems like it still overwriting the current data, I wonder what would be the issue. – Naufal Yahaya Jan 23 '15 at 02:33
  • I misread your original post, with the way you did the assignment I didn't realize 'following' was an array. You need to $push the value instead of $set. I've modified my answer. – Yuri Zarubin Jan 23 '15 at 02:42
  • @yzarubin sorry for asking you so many questions but I received another error: **The field 'following' must be an array but is of type OID in document {_id: ObjectId('54bd6b90b7d4cc8c10b40cbd')}** (POSTMAN error) and error I received on terminal is : **throw new Error('Can\'t set headers after they are sent.');** – Naufal Yahaya Jan 23 '15 at 02:47
  • It's because when we did $set, we overwrote the original array, so now 'following' is no longer an array. You will need to delete or re-create the document, so that 'following' is an array again. – Yuri Zarubin Jan 23 '15 at 02:56
  • @yzarubin I didnt get to say thank you, your answers always helped me but this time I'm sorry I couldn't accept your answer since there is a better and more accurate answer. Thank you – Naufal Yahaya Jan 23 '15 at 03:50