Skip to content

Commit aaa24e4

Browse files
asdf
1 parent 00cb86b commit aaa24e4

File tree

11 files changed

+607
-58
lines changed

11 files changed

+607
-58
lines changed

package-lock.json

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"mocha": "^10.4.0",
9898
"mocha-sinon": "^2.1.2",
9999
"mongodb-client-encryption": "^6.1.0",
100-
"mongodb-legacy": "^6.1.2",
100+
"mongodb-legacy": "^6.1.3",
101101
"nyc": "^15.1.0",
102102
"prettier": "^3.3.3",
103103
"semver": "^7.6.3",

src/cursor/aggregation_cursor.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Document } from '../bson';
22
import { MongoAPIError } from '../error';
3-
import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain';
3+
import {
4+
Explain,
5+
type ExplainCommandOptions,
6+
type ExplainVerbosityLike,
7+
validateExplainTimeoutOptions
8+
} from '../explain';
49
import type { MongoClient } from '../mongo_client';
510
import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
611
import { executeOperation } from '../operations/execute_operation';
@@ -65,26 +70,66 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
6570

6671
/** @internal */
6772
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
68-
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, {
73+
const options = {
6974
...this.aggregateOptions,
7075
...this.cursorOptions,
7176
session
72-
});
77+
};
78+
try {
79+
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
80+
} catch {
81+
throw new MongoAPIError(
82+
'timeoutMS cannot be used with explain when explain is specified in aggregateOptions'
83+
);
84+
}
85+
86+
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, options);
7387

7488
const response = await executeOperation(this.client, aggregateOperation, this.timeoutContext);
7589

7690
return { server: aggregateOperation.server, session, response };
7791
}
7892

7993
/** Execute the explain for the cursor */
80-
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
94+
async explain(): Promise<Document>;
95+
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
96+
async explain(options: { timeoutMS?: number }): Promise<Document>;
97+
async explain(
98+
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
99+
options: { timeoutMS?: number }
100+
): Promise<Document>;
101+
async explain(
102+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
103+
options?: { timeoutMS?: number }
104+
): Promise<Document> {
105+
let explain: ExplainVerbosityLike | ExplainCommandOptions | undefined;
106+
let timeout: { timeoutMS?: number } | undefined;
107+
if (verbosity == null && options == null) {
108+
explain = true;
109+
timeout = undefined;
110+
} else if (verbosity != null && options == null) {
111+
explain =
112+
typeof verbosity !== 'object'
113+
? verbosity
114+
: 'verbosity' in verbosity
115+
? verbosity
116+
: undefined;
117+
118+
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
119+
} else {
120+
// @ts-expect-error TS isn't smart enough to determine that if both options are provided, the first is explain options
121+
explain = verbosity;
122+
timeout = options;
123+
}
124+
81125
return (
82126
await executeOperation(
83127
this.client,
84128
new AggregateOperation(this.namespace, this.pipeline, {
85129
...this.aggregateOptions, // NOTE: order matters here, we may need to refine this
86130
...this.cursorOptions,
87-
explain: verbosity ?? true
131+
...timeout,
132+
explain: explain ?? true
88133
})
89134
)
90135
).shift(this.deserializationOptions);

src/cursor/find_cursor.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { type Document } from '../bson';
22
import { CursorResponse } from '../cmap/wire_protocol/responses';
3-
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
4-
import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain';
3+
import { MongoAPIError, MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
4+
import {
5+
Explain,
6+
type ExplainCommandOptions,
7+
type ExplainVerbosityLike,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
510
import type { MongoClient } from '../mongo_client';
611
import type { CollationOptions } from '../operations/command';
712
import { CountOperation, type CountOptions } from '../operations/count';
@@ -63,11 +68,21 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
6368

6469
/** @internal */
6570
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
66-
const findOperation = new FindOperation(this.namespace, this.cursorFilter, {
71+
const options = {
6772
...this.findOptions, // NOTE: order matters here, we may need to refine this
6873
...this.cursorOptions,
6974
session
70-
});
75+
};
76+
77+
try {
78+
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
79+
} catch {
80+
throw new MongoAPIError(
81+
'timeoutMS cannot be used with explain when explain is specified in findOptions'
82+
);
83+
}
84+
85+
const findOperation = new FindOperation(this.namespace, this.cursorFilter, options);
7186

7287
const response = await executeOperation(this.client, findOperation, this.timeoutContext);
7388

@@ -133,14 +148,44 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
133148
}
134149

135150
/** Execute the explain for the cursor */
136-
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
151+
async explain(): Promise<Document>;
152+
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
153+
async explain(options: { timeoutMS?: number }): Promise<Document>;
154+
async explain(
155+
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
156+
options: { timeoutMS?: number }
157+
): Promise<Document>;
158+
async explain(
159+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
160+
options?: { timeoutMS?: number }
161+
): Promise<Document> {
162+
let explain: ExplainVerbosityLike | ExplainCommandOptions | undefined;
163+
let timeout: { timeoutMS?: number } | undefined;
164+
if (verbosity == null && options == null) {
165+
explain = true;
166+
timeout = undefined;
167+
} else if (verbosity != null && options == null) {
168+
explain =
169+
typeof verbosity !== 'object'
170+
? verbosity
171+
: 'verbosity' in verbosity
172+
? verbosity
173+
: undefined;
174+
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
175+
} else {
176+
// @ts-expect-error TS isn't smart enough to determine that if both options are provided, the first is explain options
177+
explain = verbosity;
178+
timeout = options;
179+
}
180+
137181
return (
138182
await executeOperation(
139183
this.client,
140184
new FindOperation(this.namespace, this.cursorFilter, {
141185
...this.findOptions, // NOTE: order matters here, we may need to refine this
142186
...this.cursorOptions,
143-
explain: verbosity ?? true
187+
...timeout,
188+
explain: explain ?? true
144189
})
145190
)
146191
).shift(this.deserializationOptions);

src/explain.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { type Document } from 'bson';
2+
3+
import { MongoAPIError } from './error';
4+
15
/** @public */
26
export const ExplainVerbosity = Object.freeze({
37
queryPlanner: 'queryPlanner',
@@ -86,3 +90,36 @@ export class Explain {
8690
return new Explain(verbosity, maxTimeMS);
8791
}
8892
}
93+
94+
export function validateExplainTimeoutOptions(options: Document, explain?: Explain) {
95+
const { maxTimeMS, timeoutMS } = options;
96+
if (timeoutMS != null && (maxTimeMS != null || explain?.maxTimeMS != null)) {
97+
throw new MongoAPIError('Cannot use maxTimeMS with timeoutMS for explain commands.');
98+
}
99+
}
100+
101+
/**
102+
* Applies an explain to a given command.
103+
* @internal
104+
*
105+
* @param command - the command on which to apply the explain
106+
* @param options - the options containing the explain verbosity
107+
*/
108+
export function decorateWithExplain(
109+
command: Document,
110+
explain: Explain
111+
): {
112+
explain: Document;
113+
verbosity: ExplainVerbosity;
114+
maxTimeMS?: number;
115+
} {
116+
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
117+
const { verbosity, maxTimeMS } = explain;
118+
const baseCommand: ExplainCommand = { explain: command, verbosity };
119+
120+
if (typeof maxTimeMS === 'number') {
121+
baseCommand.maxTimeMS = maxTimeMS;
122+
}
123+
124+
return baseCommand;
125+
}

src/operations/command.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import type { BSONSerializeOptions, Document } from '../bson';
22
import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses';
33
import { MongoInvalidArgumentError } from '../error';
4-
import { Explain, type ExplainOptions } from '../explain';
4+
import {
5+
decorateWithExplain,
6+
Explain,
7+
type ExplainOptions,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
510
import { ReadConcern } from '../read_concern';
611
import type { ReadPreference } from '../read_preference';
712
import type { Server } from '../sdam/server';
813
import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection';
914
import type { ClientSession } from '../sessions';
1015
import { type TimeoutContext } from '../timeout';
11-
import {
12-
commandSupportsReadConcern,
13-
decorateWithExplain,
14-
maxWireVersion,
15-
MongoDBNamespace
16-
} from '../utils';
16+
import { commandSupportsReadConcern, maxWireVersion, MongoDBNamespace } from '../utils';
1717
import { WriteConcern, type WriteConcernOptions } from '../write_concern';
1818
import type { ReadConcernLike } from './../read_concern';
1919
import { AbstractOperation, Aspect, type OperationOptions } from './operation';
@@ -97,6 +97,7 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {
9797

9898
if (this.hasAspect(Aspect.EXPLAINABLE)) {
9999
this.explain = Explain.fromOptions(options);
100+
validateExplainTimeoutOptions(this.options, this.explain);
100101
} else if (options?.explain != null) {
101102
throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`);
102103
}

src/operations/find.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import type { Document } from '../bson';
22
import { CursorResponse, ExplainedCursorResponse } from '../cmap/wire_protocol/responses';
33
import { type AbstractCursorOptions, type CursorTimeoutMode } from '../cursor/abstract_cursor';
44
import { MongoInvalidArgumentError } from '../error';
5-
import { type ExplainOptions } from '../explain';
5+
import {
6+
decorateWithExplain,
7+
type ExplainOptions,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
610
import { ReadConcern } from '../read_concern';
711
import type { Server } from '../sdam/server';
812
import type { ClientSession } from '../sessions';
913
import { formatSort, type Sort } from '../sort';
1014
import { type TimeoutContext } from '../timeout';
11-
import { decorateWithExplain, type MongoDBNamespace, normalizeHintField } from '../utils';
15+
import { type MongoDBNamespace, normalizeHintField } from '../utils';
1216
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
1317
import { Aspect, defineAspects, type Hint } from './operation';
1418

@@ -119,6 +123,7 @@ export class FindOperation extends CommandOperation<CursorResponse> {
119123

120124
let findCommand = makeFindCommand(this.ns, this.filter, options);
121125
if (this.explain) {
126+
validateExplainTimeoutOptions(this.options, this.explain);
122127
findCommand = decorateWithExplain(findCommand, this.explain);
123128
}
124129

src/utils.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
MongoParseError,
2626
MongoRuntimeError
2727
} from './error';
28-
import type { Explain, ExplainVerbosity } from './explain';
2928
import type { MongoClient } from './mongo_client';
3029
import type { CommandOperationOptions, OperationParent } from './operations/command';
3130
import type { Hint, OperationOptions } from './operations/operation';
@@ -245,32 +244,6 @@ export function decorateWithReadConcern(
245244
}
246245
}
247246

248-
/**
249-
* Applies an explain to a given command.
250-
* @internal
251-
*
252-
* @param command - the command on which to apply the explain
253-
* @param options - the options containing the explain verbosity
254-
*/
255-
export function decorateWithExplain(
256-
command: Document,
257-
explain: Explain
258-
): {
259-
explain: Document;
260-
verbosity: ExplainVerbosity;
261-
maxTimeMS?: number;
262-
} {
263-
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
264-
const { verbosity, maxTimeMS } = explain;
265-
const baseCommand: ExplainCommand = { explain: command, verbosity };
266-
267-
if (typeof maxTimeMS === 'number') {
268-
baseCommand.maxTimeMS = maxTimeMS;
269-
}
270-
271-
return baseCommand;
272-
}
273-
274247
/**
275248
* @internal
276249
*/

0 commit comments

Comments
 (0)