Mongoose.js: Atomic update of nested properties?

Using Mongoose version 3.6.4

Say I have a MongoDB document like so:

{
    "_id" : "5187b74e66ee9af96c39d3d6",
    "profile" : {
        "name" : {
            "first" : "Joe",
            "last" : "Pesci",
            "middle" : "Frank"
        }
    }
}

And I have the following schema for Users:

var UserSchema = new mongoose.Schema({
  _id:    { type: String },
  email:  { type: String, required: true, index: { unique: true }},
  active: { type: Boolean, required: true, 'default': false },
  profile: {
    name: {
      first:    { type: String, required: true },
      last:     { type: String, required: true },
      middle:   { type: String }
    }
  }
  created:    { type: Date, required: true, 'default': Date.now},
  updated:    { type: Date, required: true, 'default': Date.now}
);

And I submit a form passing a field named: profile[name][first] with a value of Joseph

and thus I want to update just the user's first name, but leave his last and middle alone, I thought I would just do:

User.update({email: "joe@foo.com"}, req.body, function(err, result){});

But when I do that, it "deletes" the profile.name.last and profile.name.middle properties and I end up with a doc that looks like:

{
    "_id" : "5187b74e66ee9af96c39d3d6",
    "profile" : {
        "name" : {
            "first" : "Joseph"
        }
    }
}

So it's basically overwriting all of profile with req.body.profile, which I guess makes sense. Is there any way around it without having to be more explicit by specifying my fields in the update query instead of req.body?

You are correct, Mongoose converts updates to $set for you. But this doesn't solve your issue. Try it out in the mongodb shell and you'll see the same behavior.

Instead, to update a single deeply nested property you need to specify the full path to the deep property in the $set.

User.update({ email: 'joe@foo.com' }, { 'profile.name.first': 'Joseph' }, callback)

I think you are looking for $set

http://docs.mongodb.org/manual/reference/operator/set/

User.update({email: "joe@foo.com"}, { $set : req.body}, function(err, result){});

Try that

Maybe it's a good solution - add option to Model.update, that replace nested objects like:

{field1: 1, fields2: {a: 1, b:2 }} => {'field1': 1, 'field2.a': 1, 'field2.b': 2}

  nestedToDotNotation: function(obj, keyPrefix) {
    var result;
    if (keyPrefix == null) {
      keyPrefix = '';
    }
    result = {};
    _.each(obj, function(value, key) {
      var nestedObj, result_key;
      result_key = keyPrefix + key;
      if (!_.isArray(value) && _.isObject(value)) {
        result_key += '.';
        nestedObj = module.exports.nestedToDotNotation(value, result_key);
        return _.extend(result, nestedObj);
      } else {
        return result[result_key] = value;
      }
    });
    return result;
  }

});

need improvements circular reference handling, but this is really useful when working with nested objects

I'm using underscore.js here, but these functions easily can be replaced with other analogs

One very easy way to solve this with Moongose 4.1 and the flat package:

var flat = require('flat'),
    Schema = mongoose.Schema,
        schema = new Schema(
            {
                name: {
                    first: {
                        type: String,
                        trim: true
                    },
                    last: {
                        type: String,
                        trim: true
                    }
                }
            }
        );

    schema.pre('findOneAndUpdate', function () {
        this._update = flat(this._update);
    });


    mongoose.model('User', schema);

req.body (for example) can now be:

{
    name: {
        first: 'updatedFirstName'
    }
}

The object will be flattened before the actual query is executed, thus $set will update only the expected properties instead of the entire name object.