diff --git a/src/collection.ts b/src/collection.ts index 459ecd34226..0b647826f05 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -25,7 +25,6 @@ import type { import type { AggregateOptions } from './operations/aggregate'; import { BulkWriteOperation } from './operations/bulk_write'; import { CountOperation, type CountOptions } from './operations/count'; -import { CountDocumentsOperation, type CountDocumentsOptions } from './operations/count_documents'; import { DeleteManyOperation, DeleteOneOperation, @@ -101,6 +100,14 @@ export interface ModifyResult { ok: 0 | 1; } +/** @public */ +export interface CountDocumentsOptions extends AggregateOptions { + /** The number of documents to skip. */ + skip?: number; + /** The maximum amounts to count before aborting. */ + limit?: number; +} + /** @public */ export interface CollectionOptions extends BSONSerializeOptions, WriteConcernOptions { /** Specify a read concern for the collection. (only MongoDB 3.2 or higher supported) */ @@ -764,10 +771,23 @@ export class Collection { filter: Filter = {}, options: CountDocumentsOptions = {} ): Promise { - return await executeOperation( - this.client, - new CountDocumentsOperation(this as TODO_NODE_3286, filter, resolveOptions(this, options)) - ); + const pipeline = []; + pipeline.push({ $match: filter }); + + if (typeof options.skip === 'number') { + pipeline.push({ $skip: options.skip }); + } + + if (typeof options.limit === 'number') { + pipeline.push({ $limit: options.limit }); + } + + pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } }); + + const cursor = this.aggregate<{ n: number }>(pipeline, options); + const doc = await cursor.next(); + await cursor.close(); + return doc?.n ?? 0; } /** diff --git a/src/index.ts b/src/index.ts index 1bd801518c8..8bf6c686174 100644 --- a/src/index.ts +++ b/src/index.ts @@ -305,7 +305,12 @@ export type { MongoDBResponse, MongoDBResponseConstructor } from './cmap/wire_protocol/responses'; -export type { CollectionOptions, CollectionPrivate, ModifyResult } from './collection'; +export type { + CollectionOptions, + CollectionPrivate, + CountDocumentsOptions, + ModifyResult +} from './collection'; export type { COMMAND_FAILED, COMMAND_STARTED, @@ -463,7 +468,6 @@ export type { OperationParent } from './operations/command'; export type { CountOptions } from './operations/count'; -export type { CountDocumentsOptions } from './operations/count_documents'; export type { ClusteredCollectionOptions, CreateCollectionOptions, diff --git a/src/operations/count_documents.ts b/src/operations/count_documents.ts deleted file mode 100644 index 62273ad0228..00000000000 --- a/src/operations/count_documents.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Document } from '../bson'; -import type { Collection } from '../collection'; -import { type TODO_NODE_3286 } from '../mongo_types'; -import type { Server } from '../sdam/server'; -import type { ClientSession } from '../sessions'; -import { AggregateOperation, type AggregateOptions } from './aggregate'; - -/** @public */ -export interface CountDocumentsOptions extends AggregateOptions { - /** The number of documents to skip. */ - skip?: number; - /** The maximum amounts to count before aborting. */ - limit?: number; -} - -/** @internal */ -export class CountDocumentsOperation extends AggregateOperation { - constructor(collection: Collection, query: Document, options: CountDocumentsOptions) { - const pipeline = []; - pipeline.push({ $match: query }); - - if (typeof options.skip === 'number') { - pipeline.push({ $skip: options.skip }); - } - - if (typeof options.limit === 'number') { - pipeline.push({ $limit: options.limit }); - } - - pipeline.push({ $group: { _id: 1, n: { $sum: 1 } } }); - - super(collection.s.namespace, pipeline, options); - } - - override async execute( - server: Server, - session: ClientSession | undefined - ): Promise { - const result = await super.execute(server, session); - return result.shift()?.n ?? 0; - } -} diff --git a/test/integration/crud/crud_api.test.ts b/test/integration/crud/crud_api.test.ts index 55c39f9bd16..a391e1c4487 100644 --- a/test/integration/crud/crud_api.test.ts +++ b/test/integration/crud/crud_api.test.ts @@ -156,6 +156,87 @@ describe('CRUD API', function () { }); }); + describe('countDocuments()', () => { + let client: MongoClient; + let events; + let collection: Collection<{ _id: number }>; + + beforeEach(async function () { + client = this.configuration.newClient({ monitorCommands: true }); + events = []; + client.on('commandSucceeded', commandSucceeded => + commandSucceeded.commandName === 'aggregate' ? events.push(commandSucceeded) : null + ); + client.on('commandFailed', commandFailed => + commandFailed.commandName === 'aggregate' ? events.push(commandFailed) : null + ); + + collection = client.db('countDocuments').collection('countDocuments'); + await collection.drop().catch(() => null); + await collection.insertMany([{ _id: 1 }, { _id: 2 }]); + }); + + afterEach(async () => { + await collection.drop().catch(() => null); + await client.close(); + }); + + describe('when the aggregation operation succeeds', () => { + it('the cursor for countDocuments is closed', async function () { + const spy = sinon.spy(Collection.prototype, 'aggregate'); + const result = await collection.countDocuments({}); + expect(result).to.deep.equal(2); + expect(events[0]).to.be.instanceOf(CommandSucceededEvent); + expect(spy.returnValues[0]).to.have.property('closed', true); + expect(spy.returnValues[0]).to.have.nested.property('session.hasEnded', true); + }); + }); + + describe('when the aggregation operation fails', () => { + beforeEach(async function () { + if (semver.lt(this.configuration.version, '4.2.0')) { + if (this.currentTest) { + this.currentTest.skipReason = `Cannot run fail points on server version: ${this.configuration.version}`; + } + this.skip(); + } + + const failPoint: FailPoint = { + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['aggregate'], + // 1 == InternalError, but this value not important to the test + errorCode: 1 + } + }; + await client.db().admin().command(failPoint); + }); + + afterEach(async function () { + if (semver.lt(this.configuration.version, '4.2.0')) { + return; + } + + const failPoint: FailPoint = { + configureFailPoint: 'failCommand', + mode: 'off', + data: { failCommands: ['aggregate'] } + }; + await client.db().admin().command(failPoint); + }); + + it('the cursor for countDocuments is closed', async function () { + const spy = sinon.spy(Collection.prototype, 'aggregate'); + const error = await collection.countDocuments({}).catch(error => error); + expect(error).to.be.instanceOf(MongoServerError); + expect(events.at(0)).to.be.instanceOf(CommandFailedEvent); + expect(spy.returnValues.at(0)).to.have.property('closed', true); + expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + }); + }); + }); + context('when creating a cursor with find', () => { let collection; diff --git a/test/mongodb.ts b/test/mongodb.ts index 2d44f357863..887c65d2774 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -157,7 +157,6 @@ export * from '../src/operations/bulk_write'; export * from '../src/operations/collections'; export * from '../src/operations/command'; export * from '../src/operations/count'; -export * from '../src/operations/count_documents'; export * from '../src/operations/create_collection'; export * from '../src/operations/delete'; export * from '../src/operations/distinct';