Changes to the findOneAnd* APIs in Node.js Driver 6.0.0

Alex Bevilacqua

Do you use the MongoDB Node.js driver? If so, there’s a good chance you use various find() operations regularly.

MongoDB plans to release version 6.0.0 of the Node.js driver in August 2023, and we’ve made some exciting improvements to the findOneAnd* operation. With the new driver release, the modified (or original) document targeted by a findOneAnd* operation will now be returned by default.

Current state

Up until now, as opposed to returning the requested document, this family of API methods would return a ModifyResult, which would contain the requested document in a value field. This design was due to these APIs leveraging the MongoDB Server’s findOneAndModify command and wrapping the command’s output directly.

To demonstrate, let’s adapt the code from the Driver’s documented usage examples to update one document in our movies collection using the findOneAndUpdate API.

const database = client.db("sample_mflix");
const movies = database.collection("movies");
// Query for a movie that has the title 'The Room'
const query = { title: "The Room" };
const updatedMovie = await movies.findOneAndUpdate(query,
  { $set: { "imdb.rating": 3.4, "imdb.votes": 25750 } },
  { projection: { _id: 0, title: 1, imdb: 1 }, returnDocument: "after" });
console.log(updatedMovie);

{
  lastErrorObject: { n: 1, updatedExisting: true },
  value: {
    title: 'The Room',
    imdb: { rating: 3.4, votes: 25750, id: 368226 }
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: new Timestamp({ t: 1689343889, i: 2 }),
    signature: {
      hash: Binary.createFromBase64("3twlRKhDSGIW25WVHZl17EV2ulM=", 0),
      keyId: new Long("7192273593030410245")
    }
  },
  operationTime: new Timestamp({ t: 1689343889, i: 2 })
}

One of the options we set was a returnDocument of after, which should return the updated document. Though the expectation may be that the function call would return the document directly, as we can see this isn’t the case.

While the document you’re looking for can be accessed using updatedMovie.value, that isn’t the most intuitive experience. But changes are on the way!

What can we do right now?

Starting with the Node.js Driver 5.7.0 release a new FindOneAnd*Options property called includeResultMetadata has been introduced. When this property is set to false (default is true) the findOneAnd* APIs will return the requested document as expected.

const updatedMovie = await movies.findOneAndUpdate(query,
  { $set: { "imdb.rating": 3.3, "imdb.votes": 25999 } },
  { projection: { _id: 0, title: 1, imdb: 1 }, includeResultMetadata: false });
console.dir(updatedMovie);

{ title: 'The Room', imdb: { rating: 3.3, votes: 25999, id: 368226 } }

What about TypeScript?

If your application uses TypeScript and the MongoDB Node.js Driver, anywhere a findOneAnd* call is made, if the requested document is required it will be accessed via the value property of the ModifyResult. This occurs when includeResultMetadata is not set or when it is set to true (the current default value).

Type hinting will indicate the Schema associated with the collection the operation was executed against. As we would expect, when the includeResultMetadata is changed to false inline validation will indicate there’s an issue as the value property no longer exists on the type associated with the result.

Attempting to compile our TypeScript project will also fail.

TSError: ⨯ Unable to compile TypeScript:
index.ts:31:17 - error TS18047: 'updatedMovie' is possibly 'null'.

31     console.dir(updatedMovie.value);
                   ~~~~~~~~~~~~
index.ts:31:30 - error TS2339: Property 'value' does not exist on type 'WithId<Movie>'.

31     console.dir(updatedMovie.value);

Next Steps

If you’re using the findOneAnd* family of APIs in your JavaScript or TypeScript project, upgrading the MongoDB Node.js Driver to 5.7.0+ and adding the includeResultMetadata: false option to those API calls will allow you to adapt your application to the new behavior prior to the 6.0.0 release.

Once 6.0.0 is released, includeResultMetadata: false will become the default behavior. If your application relies on the previous behavior of these APIs, setting includeResultMetadata: true will allow you to continue to access the ModifyResult directly.