1

Should this work? I am trying to remove a single subdocument (following) from a document (this) in the UserSchema model.

UserSchema.methods.unFollow = function( id ) {
var user = this

return Q.Promise( function ( resolve, reject, notify ) {
    var unFollow = user.following.pull( { 'user': id } )

    console.log( unFollow )

    user.save( function ( error, result ) {
        resolve( result )   
    })  
})
}

These are the schemas:

var Follows = new mongoose.Schema({
    user: String,
    added: Number
})

var UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    following: [ Follows ]
})

user-controller.js

/*
Unfollow user.
*/
exports.unFollow = function ( req, res ) {

    User.findOne( { token: req.token }, function ( error, user ) {
        user.unfollow( req.body.id )
        .onResolve( function ( err, result ) {
            if ( err || !result ) return res.status( 500 ).json( "User could not be unfollowed." )

            return res.status( 200 ).json( "User unfollowed." )
        })
    })
}

user-model.js

/*
Unfollow a user.
*/
UserSchema.method( 'unfollow', function unfollow ( id ) {
    this.following.pull( { user: id } )

    return this.save()
})
Noah
  • 4,601
  • 9
  • 39
  • 52

2 Answers2

3

You generally assign methods using the method function:

UserSchema.method('unFollow', function unFollow(id) {
  var user = this;

  user.following.pull({_id: id});
  // Returns a promise in Mongoose 4.X
  return user.save();
});

Also, as noted, you don't need to use Q as save will return a mongoose promise.

UPDATE: Mongoose's array pull method will work with matching primitive values but with subdocument objects it will only match on _id.

UPDATE #2: I just noticed your updated question shows that your controller is doing a lookup first, modifying the returned document and then saving the document back to the server. Why not create a static rather than a method to do what you want? This has the added bonus of being a single call to the DB rather than two per operation.

Example:

UserSchema.static('unfollow', function unfollow(token, id, cb) {
  var User = this;

  // Returns a promise in Mongoose 4.X
  // or call cb if provided
  return User.findOneAndUpdate({token: token}, {$pull: {follows: {user: id}}}, {new: true}).exec(cb);
});

User.unfollow(req.token, req.body.id).onResolve(function (err, result) {
  if (err || !result) { return res.status(500).json({msg: 'User could not be unfollowed.'}); }

  return res.status(200).json({msg: 'User unfollowed.'})
});

Bonus follow static:

UserSchema.static('follow', function follow(token, id, cb) {
  var User = this;

  // Returns a promise in Mongoose 4.X
  // or call cb if provided
  return User.findOneAndUpdate({token: token}, {$push: {follows: {user: id}}}, {new: true}).exec(cb);
});

User.follow(req.token, req.body.id).onResolve(function (err, result) {
  if (err || !result) { return res.status(500).json({msg: 'User could not be followed.'}); }

  return res.status(200).json({msg: 'User followed.'})
});
Jason Cust
  • 10,743
  • 2
  • 33
  • 45
  • Thanks, but this still does not remove the subdocument where the `user` field is the `id`. It seems to return the object affected as it was before a pull. But the subdocument is question is still there. – Noah Apr 08 '15 at 04:38
  • Which version of mongoose are you using? This works with mongoose 4.0.1 with `user.unfollow(userToUnfollow).onResolve(function (err, user) {...})` – Jason Cust Apr 08 '15 at 04:53
  • I'm using `4.0.1` (just upgraded). I've added my controller code above. – Noah Apr 08 '15 at 05:24
  • Also tried `this.update( { $pull: { following: { user: id } } } )` Seems to execute without error but nothing is removed. – Noah Apr 08 '15 at 06:00
  • Apologies. I was using a different schema where the `following` subdocument was just the `user`. Mongoose's `pull` only matches on `_id` so you would need to pass that in and match on that. – Jason Cust Apr 08 '15 at 06:13
  • Thanks. I was worried that was the case. – Noah Apr 08 '15 at 06:29
1

NOTE: Used in "mongoose": "^5.12.13".

As for today June 22nd, 2021, you can use $in and $pull mongodb operators to remove items from an array of documents :

Parent Document :

{
    "name": "June Grocery",
    "description": "Some description",
    "createdDate": "2021-06-09T20:17:29.029Z",
    "_id": "60c5f64f0041190ad312b419",
    "items": [],
    "budget": 1500,
    "owner": "60a97ea7c4d629866c1d99d1",
}

Documents in Items array :

        {
            "category": "Fruits",
            "bought": false,
            "id": "60ada26be8bdbf195887acc1",
            "name": "Kiwi",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Toiletry",
            "bought": false,
            "id": "60b92dd67ae0934c8dfce126",
            "name": "Toilet Paper",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Toiletry",
            "bought": false,
            "id": "60b92fe97ae0934c8dfce127",
            "name": "Toothpaste",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Toiletry",
            "bought": false,
            "id": "60b92ffb7ae0934c8dfce128",
            "name": "Mouthwash",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Toiletry",
            "bought": false,
            "id": "60b931fa7ae0934c8dfce12d",
            "name": "Body Soap",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Fruit",
            "bought": false,
            "id": "60b9300c7ae0934c8dfce129",
            "name": "Banana",
            "price": 0,
            "quantity": 1
        },
        {
            "category": "Vegetable",
            "bought": false,
            "id": "60b930347ae0934c8dfce12a",
            "name": "Sombe",
            "price": 0,
            "quantity": 1
        },

Query :

MyModel.updateMany(
    { _id: yourDocumentId },
    { $pull: { items: { id: { $in: itemIds } } } },
    { multi: true }
  );

Note: ItemIds is an array of ObjectId. See below :

[
  '60ada26be8bdbf195887acc1',
  '60b930347ae0934c8dfce12a',
  '60b9300c7ae0934c8dfce129'
]