Search code examples
javascriptnode.jsmongodbmongoose

Mongodb get list of IDs for only the array objects that were updated


Here is an example of my collection "messages".

{[
  _id: xxx,
  shipment: {
   _id: xxx
  },
  messages: [
    { 
      _id: 123,
      origin: 'driver'
      isRead: false,
      ...
    },
    { 
      _id: 234,
      origin: 'driver'
      isRead: false,
      ...
    },
    { 
      _id: 345,
      origin: 'driver'
      isRead: true,
      ...
    },
    { 
      _id: 456,
      origin: 'dispatch'
      isRead: false,
      ...
    },
  ]
]}

I'm updating the array objects using the following code which works great, however this returns the entire document back with all array objects, and I need to return a list of only the objects that were updated so I can pull a list of their IDs.

const message = await MessageModel.findOneAndUpdate(
{
  _id: messageId
},
{
  $set: {
    'messages.$[elem].isRead': true
  }
},
{
  arrayFilters: [{
    'elem.isRead': false,
    'elem.origin': 'driver'
  }],
  new: true
}
);

Output I need in some form so I can grab the IDs that were updated to process further:

messages: [
    { 
      _id: 123,
      origin: 'driver'
      isRead: true,
      ...
    },
    { 
      _id: 234,
      origin: 'driver'
      isRead: true,
      ...
    }
]

const ids = [123,234];

I understand it's returning the entire document, but I can't figure out the best route to grab only what's updated without doing multiple queries and comparing.

I tried working with aggregate but am new to that and can't figure it out.

Any help greatly appreciated on the best route.


Solution

  • MongoDB updates work on documents. findOneAndUpdate returns original document (whole document) or an updated document (whole document) based on the projection and option flag - So you cannot get only the updated messages fields.

    But if your goal is to avoid multiple db queries and yet be able to find out updated fields, then you can do following -

    1. Run the above query without new: true
    2. This will return the original document before the update. This is useful as MongoDB returns the original document only if the operation is successful.
    3. Iterate over messages fields to extract updated fields

    Sample code -

    const message = await MessageModel.findOneAndUpdate(
        {
          _id: messageId,
          // Note below conditions are same as filters below
          'messages.isRead': false,
          'messages.origin': 'driver'
        },
        {
          $set: {
            'messages.$[elem].isRead': true
          }
        },
        {
          arrayFilters: [{
            'elem.isRead': false,
            'elem.origin': 'driver'
          }]
        }
    );
    // message will be the original document before update
    
    const updatedIds = []
    for (let i=0; i<message?.messages?.length; i++) {
        if (message.messages[i].isRead === false && message.messages[i].origin === 'driver') {
            // This means this field got update
            updatedIds.push(message.messages[i]._id);
        }
    }
    
    // return or use updatedIds
    

    Reason for including isRead & origin in where clause -

    As we want to update the document based on these flags only and we are interested in the doc only if an update happens, it makes sense to include respective filters in the where clause. This way, the query goes to the next step (update) only if at least one element of the messages array is meeting the criteria.