evanmcd
evanmcd

Reputation: 1969

Selecting and updating a nested object by it's ObjectId in Mongoose.js

I'm having trouble with something that thought would be trivial in MongoDB with Mongoose.

With a fairly simple schema like this

const UserSchema = new Schema({
groups: [
    {
        name: String,
        members: [
            {
                hasAccepted: {
                    type: Boolean
                }
            }
        ]
    }
]
});

When i create new groups, each member object gets an _id property of course. I simply want to select that member by its _id and update its hasAccepted property.

When I run a query with the _id of the member, I get back the entire record for the user, which makes it difficult to find the nested member to update it.

How can I trim the result down to just the member with the found ID and update its property?

I'm using Mongo 3.6.2 and have tried the new arrayFilters, but with no luck.

My code (using Node) is below, which returns the whole document, but with nothing updated.

const query = {
    groups : {
        $elemMatch : { members : { $elemMatch : {_id : <id>} } }
    }
};

const update =  {$set: {'groups.$[].members.$[o].hasAccepted':true }};
const options = { new: true, arrayFilters:[{"o._id":<id>}] };

// Find the document
User.findOneAndUpdate(query, update, options, function(error, result) {
    if (error) {
        res.send(error);
    } else {
        res.send(result);
    }
});

EDIT: here's the full data from the test db i'm working with. The _id I've been testing with is one the for the member in Group 1: 5a753f168b5b7f0231ab0621

    [
{
    "_id": {
    "$oid": "5a7505452f93de2c90f49a20"
    },
    "groups": [
    {
        "name": "Group 2",
        "_id": {
        "$oid": "5a7543b8e254ab02cd728c42"
        },
        "members": [
        {
            "user": {
            "$oid": "5a7543b8e254ab02cd728c41"
            },
            "_id": {
            "$oid": "5a7543b8e254ab02cd728c43"
            },
            "hasAccepted": false
        }
        ]
    },
    {
        "name": "Group 1",
        "_id": {
        "$oid": "5a753f168b5b7f0231ab0620"
        },
        "members": [
        {
            "user": {
            "$oid": "5a753f168b5b7f0231ab061f"
            },
            "_id": {
            "$oid": "5a753f168b5b7f0231ab0621"
            },
            "hasAccepted": false
        }
        ]
    }
    ]
},
{
    "_id": {
    "$oid": "5a753f168b5b7f0231ab061f"
    },
    "groups": [],
},
{
    "_id": {
    "$oid": "5a7543b8e254ab02cd728c41"
    },
    "groups": [],

}
]

Thanks for any help you can offer.

Upvotes: 4

Views: 5808

Answers (3)

Dylan Prem
Dylan Prem

Reputation: 174

// An easier more modern way to do this is to pass a wildcard such as /:id in your API endpoint
// Use Object.assign()
// use the save() method

// If you are using JWT 
// Otherwise find another unique identifier 
const user = UserSchema.findOne({ id: req.user.id });

for (const oldObject of user.groups) {
    if(oldObject.id === req.params.id) {
        newObject = {
            propertyName: req.body.val,
            propertyName2: req.body.val2,
            propertyName3: req.body.val3
        }

        // Update the oldObject 
        Object.assign(oldObject, newObject);
        break;
    }
}

user.save()
res.json(user);

Upvotes: 0

evanmcd
evanmcd

Reputation: 1969

OK, so it turns out the the thing I needed to understand better are arrayFilters (that, and I needed to add the group name into the data I used to get to the value I needed to updated.

The thing that helped me understand arrayFilters the best was to think of the as a sort of subquery, like is used in the SQL world. Once I got that, I was able to figure out how to write my update.

This article was also very helpful in understanding how arrayFilters are used: http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters.html

Here's the code that worked for me. Note that you need Mongo 3.6 and Mongoose 5.0.0 to get support for arrayFilters.

Also, you need to be sure to require Mongoose's ObjectId like so

const ObjectId = require('mongoose').Types.ObjectId;

Here's the rest of the working code:

const query = {
    groups : {
        $elemMatch : { members : { $elemMatch : {_id : new ObjectId("theideofmymemberobject"), hasAccepted : false} } }
    }
};

const update =  {$set: {'groups.$[group].members.$[member].hasAccepted':true } };
const options = { arrayFilters: [{ 'group.name': 'Group 3' },{'member._id': new ObjectId("theideofmymemberobject")}] };

// update the document
User.update(query, update, options, function(error, result) {
    if (error) {
        res.send(error);
    } else {
        res.send(result);
    }
});

Upvotes: 2

Saravana
Saravana

Reputation: 12817

You can try below aggregation to filter only the matching group and member from the group and member arrays

replace _id with the id to be searched, use the result to update the hasAccepted status

db.groups.aggregate(
    [
        {$addFields : {"groups" : {$arrayElemAt : [{$filter : {input : "$groups", as : "g", cond : {$in : [_id, "$$g.members._id"]}}}, 0]}}},
        {$addFields : {"groups.members" : {$filter : {input : "$groups.members", as : "gm", cond : {$eq : [_id, "$$gm._id"]}}}}}
    ]
).pretty()

Upvotes: 0

Related Questions