Skip to content

Commit 8af2884

Browse files
implementation
1 parent d26a588 commit 8af2884

File tree

8 files changed

+438
-46
lines changed

8 files changed

+438
-46
lines changed

src/cursor/aggregation_cursor.ts

Lines changed: 48 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,64 @@ 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+
: 'timeoutMS' in verbosity
115+
? undefined
116+
: (verbosity as ExplainCommandOptions);
117+
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
118+
} else {
119+
explain = verbosity as ExplainCommandOptions;
120+
timeout = options;
121+
}
122+
81123
return (
82124
await executeOperation(
83125
this.client,
84126
new AggregateOperation(this.namespace, this.pipeline, {
85127
...this.aggregateOptions, // NOTE: order matters here, we may need to refine this
86128
...this.cursorOptions,
87-
explain: verbosity ?? true
129+
...timeout,
130+
explain: explain ?? true
88131
})
89132
)
90133
).shift(this.deserializationOptions);

src/cursor/find_cursor.ts

Lines changed: 50 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,43 @@ 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+
: 'timeoutMS' in verbosity
172+
? undefined
173+
: (verbosity as ExplainCommandOptions);
174+
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
175+
} else {
176+
explain = verbosity as ExplainCommandOptions;
177+
timeout = options;
178+
}
179+
137180
return (
138181
await executeOperation(
139182
this.client,
140183
new FindOperation(this.namespace, this.cursorFilter, {
141184
...this.findOptions, // NOTE: order matters here, we may need to refine this
142185
...this.cursorOptions,
143-
explain: verbosity ?? true
186+
...timeout,
187+
explain: explain ?? true
144188
})
145189
)
146190
).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 ((maxTimeMS != null && timeoutMS) || (explain?.maxTimeMS != null && timeoutMS != 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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ 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 { decorateWithExplain, validateExplainTimeoutOptions } from '../explain';
56
import { ReadConcern } from '../read_concern';
67
import type { Server } from '../sdam/server';
78
import type { ClientSession } from '../sessions';
89
import { formatSort, type Sort } from '../sort';
910
import { type TimeoutContext } from '../timeout';
10-
import { decorateWithExplain, type MongoDBNamespace, normalizeHintField } from '../utils';
11+
import { type MongoDBNamespace, normalizeHintField } from '../utils';
1112
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
1213
import { Aspect, defineAspects, type Hint } from './operation';
1314

@@ -112,6 +113,7 @@ export class FindOperation extends CommandOperation<CursorResponse> {
112113

113114
let findCommand = makeFindCommand(this.ns, this.filter, options);
114115
if (this.explain) {
116+
validateExplainTimeoutOptions(this.options, this.explain);
115117
findCommand = decorateWithExplain(findCommand, this.explain);
116118
}
117119

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';
@@ -244,32 +243,6 @@ export function decorateWithReadConcern(
244243
}
245244
}
246245

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

0 commit comments

Comments
 (0)