Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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: "[email protected]"}, 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?

like image 223
k00k Avatar asked May 06 '13 14:05

k00k


2 Answers

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: '[email protected]' }, { 'profile.name.first': 'Joseph' }, callback)
like image 149
aaronheckmann Avatar answered Sep 22 '22 14:09

aaronheckmann


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.

like image 21
Stefan Avatar answered Sep 20 '22 14:09

Stefan