Mongodb conditionally update doubly nested array

99
December 17, 2017, at 4:20 PM

My collection looks like this. A document contains an array of suggestions. Each suggestion can have an array of emotes. A user can only emote once per suggestion. Note that a document can have multiple suggestions

{
    id: 1,
    suggestions:[
        {
            id: 463,
            emotes:[
                {
                    id: 35,
                    userId: 2
                },
                {
                    id: 23,
                    userId: 3
                },
            ]
        },
        ...
    ]
},
...

emotes can only contain 1 instance of a userId. I need to check if userId exists in emotes before pushing a new emote element.

My attempted solutions:

(I'm using the update command syntax to access MongoDB 3.6 features.)

First attempt: addToSet will not work because the emote array elements are keyed by userId, not the entire object.

db.get().command({
    update: 'channels',
    updates:[
        {
            q:{ 
                channelId, 
                  "suggestions.id": suggestionOid,
            },
            u:{
                $addToSet:
                {
                    "suggestions.$[s].emotes": data
                }
            },
            arrayFilters:[
                { 's.id': suggestionOid }
            ]
        }
    ]
})

Second attempt: The below doesn't work. The user could already have an emote for another suggestion, which will cause the $ne query condition to fail. Which means the user can only emote one suggestion per document, they should be able to emote all suggestions for a document.

db.get().command({
    update: 'channels',
    updates:[
        {
            q:{ 
                channelId, 
                  'suggestions.emotes.userId': {$ne: userId },
                  "suggestions.id": suggestionOid,
            },
            u:{
                $push:
                {
                    "suggestions.$[s].emotes": data
                }
            },
            arrayFilters:[
                { 's.id': suggestionOid }
            ]
        }
    ]
})

I am open to moving emotes to a new collection or changing its structure, if that makes things easier. I can't change anything else though.

Answer 1

I solved this by using the $elemMatch operator.

return channels.updateOne(
    {
        channelId, 
        "suggestions":{
            $elemMatch:{
                id: suggestionOid,
                'emotes.user': {$ne: user }
            }
        },
    },
    {
        $push:
        {
            "suggestions.$.emotes": data
        }
    }
)

Important bit from the mongodb docs https://docs.mongodb.com/manual/reference/operator/update/positional/

If the query matches the array using a negation operator, such as $ne, $not, or $nin, then you cannot use the positional operator to update values from this array.

However, if the negated portion of the query is inside of an $elemMatch expression, then you can use the positional operator to update this field.

Answer 2

You can try the below query in shell using all positional operator.

db.channels.update(
   { channelId },
   { $push: { "suggestions.$[s].emotes": {id: 24,userId: 3}} },
   { arrayFilters: [  { "s.id": 463, "s.emotes.userId":{ $ne: 3}} ]}
)

You can also use $elemMatch but it is not required as suggestions id uniquely identifies each suggestion.

READ ALSO
How to use ES6 import with 'request' npm module

How to use ES6 import with 'request' npm module

In ES6-ifying some TypeScript code (the project I'm working runs in both the browser and a Node server, I'd like to tree-shake the browser bundle), I'm trying to eliminate uses of require and only use importBut when I do this

152
Can't install sudo npm install -g loopback-cli on Mac osx 10.13

Can't install sudo npm install -g loopback-cli on Mac osx 10.13

First: sudo npm install -g loopback-cli

166