Skip to content
Merged
19 changes: 12 additions & 7 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
type EstimatedDocumentCountOptions
} from './operations/estimated_document_count';
import { autoConnect, executeOperation } from './operations/execute_operation';
import type { FindOptions } from './operations/find';
import { type FindOneOptions, type FindOptions } from './operations/find';
import {
FindOneAndDeleteOperation,
type FindOneAndDeleteOptions,
Expand Down Expand Up @@ -536,25 +536,30 @@ export class Collection<TSchema extends Document = Document> {
async findOne(filter: Filter<TSchema>): Promise<WithId<TSchema> | null>;
async findOne(
filter: Filter<TSchema>,
options: Omit<FindOptions, 'timeoutMode'> & Abortable
options: Omit<FindOneOptions, 'timeoutMode'> & Abortable
): Promise<WithId<TSchema> | null>;

// allow an override of the schema.
async findOne<T = TSchema>(): Promise<T | null>;
async findOne<T = TSchema>(filter: Filter<TSchema>): Promise<T | null>;
async findOne<T = TSchema>(
filter: Filter<TSchema>,
options?: Omit<FindOptions, 'timeoutMode'> & Abortable
options?: Omit<FindOneOptions, 'timeoutMode'> & Abortable
): Promise<T | null>;

async findOne(
filter: Filter<TSchema> = {},
options: FindOptions & Abortable = {}
options: Omit<FindOneOptions, 'timeoutMode'> & Abortable = {}
): Promise<WithId<TSchema> | null> {
const cursor = this.find(filter, options).limit(-1).batchSize(1);
const res = await cursor.next();
// Explicitly set the limit to 1 and singleBatch to true for all commands, per the spec.
// noCursorTimeout must be unset as well as batchSize.
// See: https://github.com/mongodb/specifications/blob/master/source/crud/crud.md#findone-api-details
const { batchSize: _batchSize, noCursorTimeout: _noCursorTimeout, ...opts } = options;
opts.singleBatch = true;
const cursor = this.find(filter, opts).limit(1);
const result = await cursor.next();
await cursor.close();
return res;
return result;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ export type { DeleteOptions, DeleteResult, DeleteStatement } from './operations/
export type { DistinctOptions } from './operations/distinct';
export type { DropCollectionOptions, DropDatabaseOptions } from './operations/drop';
export type { EstimatedDocumentCountOptions } from './operations/estimated_document_count';
export type { FindOptions } from './operations/find';
export type { FindOneOptions, FindOptions } from './operations/find';
export type {
FindOneAndDeleteOptions,
FindOneAndReplaceOptions,
Expand Down
3 changes: 2 additions & 1 deletion src/operations/execute_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,9 @@ async function tryOperation<
if (hasWriteAspect && !isRetryableWriteError(previousOperationError))
throw previousOperationError;

if (hasReadAspect && !isRetryableReadError(previousOperationError))
if (hasReadAspect && !isRetryableReadError(previousOperationError)) {
throw previousOperationError;
}

if (
previousOperationError instanceof MongoNetworkError &&
Expand Down
28 changes: 18 additions & 10 deletions src/operations/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ export interface FindOptions<TSchema extends Document = Document>
timeoutMode?: CursorTimeoutMode;
}

/** @public */
export interface FindOneOptions extends FindOptions {
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
batchSize?: number;
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
limit?: number;
/** @deprecated Will be removed in the next major version. User provided value will be ignored. */
noCursorTimeout?: boolean;
}

/** @internal */
export class FindOperation extends CommandOperation<CursorResponse> {
/**
Expand Down Expand Up @@ -185,17 +195,15 @@ function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOp

if (typeof options.batchSize === 'number') {
if (options.batchSize < 0) {
if (
options.limit &&
options.limit !== 0 &&
Math.abs(options.batchSize) < Math.abs(options.limit)
) {
findCommand.limit = -options.batchSize;
}

findCommand.singleBatch = true;
findCommand.limit = -options.batchSize;
} else {
findCommand.batchSize = options.batchSize;
if (options.batchSize === options.limit) {
// Spec dictates that if these are equal the batchSize should be one more than the
// limit to avoid leaving the cursor open.
findCommand.batchSize = options.batchSize + 1;
} else {
findCommand.batchSize = options.batchSize;
}
}
}

Expand Down
62 changes: 62 additions & 0 deletions test/spec/crud/unified/find.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,68 @@
]
}
]
},
{
"description": "Find with batchSize equal to limit",
"operations": [
{
"object": "collection0",
"name": "find",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"sort": {
"_id": 1
},
"limit": 4,
"batchSize": 4
},
"expectResult": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
}
]
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"find": "coll0",
"filter": {
"_id": {
"$gt": 1
}
},
"limit": 4,
"batchSize": 5
},
"commandName": "find",
"databaseName": "find-tests"
}
}
]
}
]
}
]
}
28 changes: 28 additions & 0 deletions test/spec/crud/unified/find.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,31 @@ tests:
- { _id: 2, x: 22 }
- { _id: 3, x: 33 }
- { _id: 4, x: 44 }
-
description: 'Find with batchSize equal to limit'
operations:
-
object: *collection0
name: find
arguments:
filter: { _id: { $gt: 1 } }
sort: { _id: 1 }
limit: 4
batchSize: 4
expectResult:
- { _id: 2, x: 22 }
- { _id: 3, x: 33 }
- { _id: 4, x: 44 }
- { _id: 5, x: 55 }
expectEvents:
- client: *client0
events:
- commandStartedEvent:
command:
find: *collection0Name
filter: { _id: { $gt: 1 } }
limit: 4
# Drivers use limit + 1 for batchSize to ensure the server closes the cursor
batchSize: 5
commandName: find
databaseName: *database0Name
158 changes: 158 additions & 0 deletions test/spec/crud/unified/findOne.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
{
"description": "findOne",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "find-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "find-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
},
{
"_id": 6,
"x": 66
}
]
}
],
"tests": [
{
"description": "FindOne with filter",
"operations": [
{
"object": "collection0",
"name": "findOne",
"arguments": {
"filter": {
"_id": 1
}
},
"expectResult": {
"_id": 1,
"x": 11
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"find": "coll0",
"filter": {
"_id": 1
},
"batchSize": {
"$$exists": false
},
"limit": 1,
"singleBatch": true
},
"commandName": "find",
"databaseName": "find-tests"
}
}
]
}
]
},
{
"description": "FindOne with filter, sort, and skip",
"operations": [
{
"object": "collection0",
"name": "findOne",
"arguments": {
"filter": {
"_id": {
"$gt": 2
}
},
"sort": {
"_id": 1
},
"skip": 2
},
"expectResult": {
"_id": 5,
"x": 55
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"find": "coll0",
"filter": {
"_id": {
"$gt": 2
}
},
"sort": {
"_id": 1
},
"skip": 2,
"batchSize": {
"$$exists": false
},
"limit": 1,
"singleBatch": true
},
"commandName": "find",
"databaseName": "find-tests"
}
}
]
}
]
}
]
}
Loading