15

Suppose I have a mongoose schema like this:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var testSchema = new Schema({
    name: {type: String, required: true},
    nickName: {type: String}
});

var Test = module.exports = mongoose.model('Test', testSchema);

I declare methods for CRUD operation using variable Test. From that one such method is update, which is defined as follows:

module.exports.updateTest = function(updatedValues, callback) {

    console.log(updatedValues); //this output is shown below

    Test.update(
        { "_id": updatedValues.id },
        { "$set" : { "name" : updatedValues.name, "nickName" : updatedValues.nickName } },
        { multi: false },
        callback
    );

};

Now, I use this method inside my node router as follows:

router.put('/:id', function(req, res, next) {

    var id = req.params.id,
    var name = req.body.name,
    var nickName = req.body.nickName

    req.checkBody("name", "Name is required").notEmpty();

    var errors = req.validationErrors();

    if(errors) { ........ }
    else {

        var testToUpdate = new Test({
            _id: id,
            name: name,
            nickName: nickName || undefined
        });

        Test.updateTest(testToUpdate, function(err, result) {
            if(err) { throw(err); }
            else { res.status(200).json({"success": "Test updated successfully"}); }
        });

    }
});

Now if I save a new record in database and then see it in database then it looks like:

{
    "_id" : ObjectId("ns8f9yyuo32hru0fu23oh"), //some automatically generated id 
    "name" : "firstTest",
    "__v" : 0
}

Now if I update the same document without changing anything and then if I take a look at same record in database, then I get:

{
    "_id" : ObjectId("ns8f9yyuo32hru0fu23oh"), //some automatically generated id 
    "name" : "firstTest",
    "__v" : 0,
    "nickName" : null
}

Can you see that nickName is set to null? I don't want it to work like this. I want that if my property is null, then that property should not be included in the record.

If you remember, I have console logged the updatedValues before updating it. (see the second code block in question). So, here is the logged values:

{
    "_id" : ObjectId("ns8f9yyuo32hru0fu23oh"), //some automatically generated id 
    "name" : "firstTest"
}

I don't know why, but nickName is not present in the logged values and then after update I get nickName: null. I think, the problem lies in second Code block. Can you please check it?

Note:

Actually I have lot more fields in my schema than I specified in question. Some fields are reference to other records as well.

Vishal
  • 6,238
  • 10
  • 82
  • 158
  • seems you want to use one of these techniques on the value of `$set`.. http://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript – lecstor Mar 18 '17 at 13:46
  • Scanning through the answers and comments, it seems that this isn't your true question, if true, please update your question so we can help you. – Anh Cao Mar 20 '17 at 17:49

7 Answers7

8

I wouldn't write this this way, but I'll tell you why your code is failing.

The problem is your $set block

You're choosing to specifically set the value to the update object passed in. If the value is undefined you're forcing mongo to set that to null.

Here's the problem

example, in DB:

{
    "_id" : ObjectId("ns8f9yyuo32hru0fu23oh"),
    "name" : "firstTest",
    "nickname": "jack",
    "__v" : 0
}

IF you pass in testToUpdate = { name: 'foo' } you'll end up with

 Test.update({ ... }, { $set: { name: 'foo', nickname: undefined }}

because you're getting updatedValues.nickname off of the arguments and thats not defined

What you want is

Test.update({ ... }, { $set: updatedValues }

which is translated to

Test.update({ ... }, { $set: { name: 'foo' } }

You're no longer providing a key for nickname, thus not making it set to undefined/null.


I would use a mongoose plugin and not worry about manually passing the fields all the way to your model (see github.com/autolotto/mongoose-model-update)

  • You can define the update-able fields and then you can just do model.update(req.body) and not worry about all this
  • Even if you don't want to use the plugin you can still just do Test.findByIdAndUpdate(id, { name, nickname }, callback)
surfmuggle
  • 5,527
  • 7
  • 48
  • 77
mrBorna
  • 1,757
  • 16
  • 16
8

You can prevent such documents from updating in MongoDB by setting the runValidators option to true in the update method.

Ex:

  module.exports.updateTest = function(updatedValues, callback) {

    Test.update(
      { "_id": updatedValues.id },
      { "$set" : { 
        "name" : updatedValues.name, "nickName" : updatedValues.nickName } ,
      },
      { multi: false, runValidators: true },
      callback
    );
  };

In addition, you can also set the option omitUndefined to true to prevent undefined values from being reflected.

Ex:

  module.exports.updateTest = function(updatedValues, callback) {

    Test.update(
      { "_id": updatedValues.id },
      { "$set" : { 
        "name" : updatedValues.name, "nickName" : updatedValues.nickName } ,
      },
      { multi: false, runValidators: true, omitUndefined: true },
      callback
    );
  };
Jatin Parate
  • 528
  • 5
  • 9
  • If you want to use `omitUndefined` in Mongoose 6, see [Mongoose docs: removed undefined](https://mongoosejs.com/docs/migrating_to_6.html#removed-omitundefined). In Mongoose 6.x, the `omitUndefined` option has been removed, and Mongoose will always strip out undefined keys. – orimdominic Aug 09 '22 at 10:04
2

Its true that the problem is your $set part as pointed in the other answers, but using if condition is not the best way to do this. What if there are multiple fields you might need multiple if conditions or even a separate function to deal with this.

Mongoose offers a really good option to deal with this:{omitUndefined: 1} , it will not update all the undefined values in your query.

Xabro
  • 23
  • 2
1

Taking a look at your code, there is a problem in your update method that won't help you to obtain the result you want.

This is you update code:

module.exports.updateTest = function(updatedValues, callback) {

    console.log(updatedValues); //this output is shown below

    Test.update(
        { "_id": updatedValues.id },
        { "$set" : { "name" : updatedValues.name, "nickName" : updatedValues.nickName } },
        { multi: false },
        callback
    );

};

The problem is in the $set part.

In mongodb you cannot unset a field in a document by just assigning undefined to it. You should use the $unset operator.

So your update code should be something like:

module.exports.updateTest = function(updatedValues, callback) {

    console.log(updatedValues); //this output is shown below
    const operators = {$set: {name: updatedValues.name}};
    if (updatedValues.nickName) {
        operators.$set.nickName = updatedValues.nickName;
    } else {
        operators.$unset = {nickName: 1};
    }
    Test.update(
        { "_id": updatedValues.id },
        operators,
        { multi: false },
        callback
    );

};

Note that the use of $unset is fundamental to remove the field if it already exists in your document.

luiso1979
  • 868
  • 1
  • 6
  • 18
  • Actually I have lot more fields in my schema than I specified in question. Some fields are reference to other records as well. Still I gave a try to your answer and I got an error when updating, which says: Cast Error: Cast to string failed for value " { ............ }", where in curly braces my object is shown. – Vishal Mar 14 '17 at 11:49
  • I also have some hierarchical objects in my schema as follows: details: { mailing: { name: String, address: string }, contact: { mobile: string, email: email }}. The object above is shown as example. How can I update it? – Vishal Mar 17 '17 at 16:31
  • Please ignore my first comment as it is solved which was my typing mistake. – Vishal Mar 17 '17 at 16:33
1

As you're using $set in the query which says in the documentation

The $set operator replaces the value of a field with the specified value.

So you're forcing to set the undefined value which is null here to the field. Either you set required : true against the nickName field in the schema or try passing JS object instead of writing raw query like, Instead of :

Test.update(
        { "_id": updatedValues.id },
        { "$set" : { "name" : updatedValues.name, "nickName" : updatedValues.nickName } },
        { multi: false },
        callback
    );

Try doing:

var data = {  name : updatedValues.name };
if(updatedValues.nickName){
     data.nickName = updatedValues.nickName;
}
Model.update({ "_id": updatedValues.id }, data ,options, callback)

Check this link in Mongoose documentation for more information on the approach.

Vishwa Bhat
  • 109
  • 3
  • I have tried to use the answer provided by you. But it gives me an error saying that: CastError: Cast to string failed for value {............}, where in curly braces is my object. – Vishal Mar 17 '17 at 16:06
  • OK, I was missing `.name` in data. Now, I also have some hierarchical objects in my schema as follows: `details: { mailing: { name: String, address: string }, contact: { mobile: string, email: email }}`. The object above is shown as example. How can I update it? – Vishal Mar 17 '17 at 16:31
  • I'm assuming you want to know how to update nested values, Ideally it should work for nested objects as well. As, In mongo, to access a nested object you'd do it in the form of .(dot) operator. – Vishwa Bhat Mar 20 '17 at 05:07
  • I have tried it with dot operator, but I am getting an error : cannot set address of undefined. The error is very much descriptive. So, I tried to set `data.details = { }; data.details.mailing = { }; data.details.mailing.address = updatedValues.address;` Now I am getting another error: cannot set mailing and address together. – Vishal Mar 20 '17 at 08:31
  • 1
    @Vishal You have to define inner object first. And I doubt if you can update multiple parameters as one parameter to $set – Vishwa Bhat Mar 20 '17 at 08:59
  • I can't understand your comment. can you please elaborate? – Vishal Mar 20 '17 at 09:03
0

The problem is that you are still adding the nickname property on the document - you're just setting it to undefined, but contrary to what you thought that's not the same as not setting it at all.

What you want is to not include the nickname property at all if it's undefined, which you can do like this:

module.exports.updateTest = function(updatedValues, callback) {

    const set = { "name": updatedValues.name };

    if (updatedValues.nickName) {
        set["nickName"] = updatedValues.nickName;
    }

    Test.update({ "_id": updatedValues.id }, { "$set": set}, { multi: false },
        callback
    );

};
Aron
  • 8,696
  • 6
  • 33
  • 59
  • Actually I have lot more fields in my schema than I specified in question. Some fields are reference to other records as well. Still I gave a try to your answer and I got an error when updating, which says: Cast Error: Cast to string failed for value " { ............ }", where in curly braces my object is shown. – Vishal Mar 14 '17 at 11:20
  • I also have some hierarchical objects in my schema as follows: details: { mailing: { name: String, address: string }, contact: { mobile: string, email: email }}. The object above is shown as example. How can I update it? – Vishal Mar 17 '17 at 16:32
  • Please ignore my first comment as it is solved which was my typing mistake. – Vishal Mar 17 '17 at 16:32
  • @Vishal ok, so does that mean my answer helped? – Aron Mar 17 '17 at 16:34
  • I don't know if your answer works or not because I got error saying cannot set address of undefined. – Vishal Mar 17 '17 at 18:57
  • My answer - and your question - doesn't include anything about an address at all so I don't know what's happened there, but it sounds like that's a separate question. First test my answer with a separate smaller schema for testing so you can check if that works. Then you can move on to the next question. – Aron Mar 19 '17 at 00:24
0

The easiest method I found is by using lodash.pickby you just pass body (or any object for that matter) and it removes all undefined and null fields and keys

tnemele12
  • 903
  • 7
  • 16