diff --git a/src/collection.ts b/src/collection.ts index 19a0682d8b2..a2df98ae2da 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -966,32 +966,40 @@ export class Collection { /** * Find a document and update it in one atomic operation. Requires a write lock for the duration of the operation. * + * The value of `update` can be either: + * - UpdateFilter - A document that contains update operator expressions, + * - Document[] - an aggregation pipeline consisting of the following stages: + * - $addFields and its alias $set + * - $project and its alias $unset + * - $replaceRoot and its alias $replaceWith. + * See the [findAndModify command documentation](https://www.mongodb.com/docs/manual/reference/command/findAndModify) for details. + * * @param filter - The filter used to select the document to update - * @param update - Update operations to be performed on the document + * @param update - The modifications to apply * @param options - Optional settings for the command */ async findOneAndUpdate( filter: Filter, - update: UpdateFilter, + update: UpdateFilter | Document[], options: FindOneAndUpdateOptions & { includeResultMetadata: true } ): Promise>; async findOneAndUpdate( filter: Filter, - update: UpdateFilter, + update: UpdateFilter | Document[], options: FindOneAndUpdateOptions & { includeResultMetadata: false } ): Promise | null>; async findOneAndUpdate( filter: Filter, - update: UpdateFilter, + update: UpdateFilter | Document[], options: FindOneAndUpdateOptions ): Promise | null>; async findOneAndUpdate( filter: Filter, - update: UpdateFilter + update: UpdateFilter | Document[] ): Promise | null>; async findOneAndUpdate( filter: Filter, - update: UpdateFilter, + update: UpdateFilter | Document[], options?: FindOneAndUpdateOptions ): Promise | ModifyResult | null> { return await executeOperation( diff --git a/test/integration/crud/find_and_modify.test.ts b/test/integration/crud/find_and_modify.test.ts index 16764f020ea..18ab93cf3da 100644 --- a/test/integration/crud/find_and_modify.test.ts +++ b/test/integration/crud/find_and_modify.test.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; -import { type CommandStartedEvent, MongoServerError, ObjectId } from '../../mongodb'; +import { + type Collection, + type CommandStartedEvent, + type MongoClient, + MongoServerError, + ObjectId +} from '../../mongodb'; import { setupDatabase } from '../shared'; describe('Collection (#findOneAnd...)', function () { @@ -324,6 +330,79 @@ describe('Collection (#findOneAnd...)', function () { }); }); }); + + context('when updating with an aggregation pipeline', function () { + context('when passing includeResultMetadata: true', function () { + let client: MongoClient; + let collection: Collection<{ a: number; b: number }>; + + beforeEach(async function () { + client = this.configuration.newClient({}, { maxPoolSize: 1 }); + collection = client.db('test').collection('findAndModifyTest'); + await collection.insertMany([{ a: 1, b: 1 }], { writeConcern: { w: 1 } }); + }); + + afterEach(async function () { + await collection.drop(); + await client?.close(); + }); + + it( + 'the aggregation pipeline updates the matching document', + { + requires: { + mongodb: '>4.0' + } + }, + async function () { + const { + value: { _id, ...document } + } = await collection.findOneAndUpdate( + { a: 1 }, + [{ $set: { a: { $add: [1, '$a'] } } }], + { + includeResultMetadata: true, + returnDocument: 'after' + } + ); + expect(document).to.deep.equal({ a: 2, b: 1 }); + } + ); + }); + + context('when passing includeResultMetadata: false', function () { + let client: MongoClient; + let collection: Collection<{ a: number; b: number }>; + + beforeEach(async function () { + client = this.configuration.newClient({}, { maxPoolSize: 1 }); + collection = client.db('test').collection('findAndModifyTest'); + await collection.insertMany([{ a: 1, b: 1 }], { writeConcern: { w: 1 } }); + }); + + afterEach(async function () { + await collection.drop(); + await client?.close(); + }); + + it( + 'the aggregation pipeline updates the matching document', + { + requires: { + mongodb: '>4.0' + } + }, + async function () { + const { _id, ...document } = await collection.findOneAndUpdate( + { a: 1 }, + [{ $set: { a: { $add: [1, '$a'] } } }], + { returnDocument: 'after' } + ); + expect(document).to.deep.equal({ a: 2, b: 1 }); + } + ); + }); + }); }); describe('#findOneAndReplace', function () { diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 8720c949400..0de99ee7d49 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -388,3 +388,14 @@ expectType | null>( } ) ); + +// the update operator can be an aggregation pipeline +expectType | null>( + await coll.findOneAndUpdate({ a: 3 }, [ + { + $set: { + a: 5 + } + } + ]) +);