Skip to content

Commit d6d76b4

Browse files
authored
feat(NODE-5207): deprecate unsupported runCommand options and add spec tests (#3643)
1 parent a16bdfa commit d6d76b4

File tree

11 files changed

+892
-24
lines changed

11 files changed

+892
-24
lines changed

src/admin.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ export class Admin {
5454
/**
5555
* Execute a command
5656
*
57+
* The driver will ensure the following fields are attached to the command sent to the server:
58+
* - `lsid` - sourced from an implicit session or options.session
59+
* - `$readPreference` - defaults to primary or can be configured by options.readPreference
60+
* - `$db` - sourced from the name of this database
61+
*
62+
* If the client has a serverApi setting:
63+
* - `apiVersion`
64+
* - `apiStrict`
65+
* - `apiDeprecationErrors`
66+
*
67+
* When in a transaction:
68+
* - `readConcern` - sourced from readConcern set on the TransactionOptions
69+
* - `writeConcern` - sourced from writeConcern set on the TransactionOptions
70+
*
71+
* Attaching any of the above fields to the command will have no effect as the driver will overwrite the value.
72+
*
5773
* @param command - The command to execute
5874
* @param options - Optional settings for the command
5975
*/

src/cmap/connection.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -484,22 +484,23 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
484484

485485
command(
486486
ns: MongoDBNamespace,
487-
cmd: Document,
487+
command: Document,
488488
options: CommandOptions | undefined,
489489
callback: Callback
490490
): void {
491-
const readPreference = getReadPreference(cmd, options);
491+
let cmd = { ...command };
492+
493+
const readPreference = getReadPreference(options);
492494
const shouldUseOpMsg = supportsOpMsg(this);
493495
const session = options?.session;
494496

495497
let clusterTime = this.clusterTime;
496-
let finalCmd = Object.assign({}, cmd);
497498

498499
if (this.serverApi) {
499500
const { version, strict, deprecationErrors } = this.serverApi;
500-
finalCmd.apiVersion = version;
501-
if (strict != null) finalCmd.apiStrict = strict;
502-
if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors;
501+
cmd.apiVersion = version;
502+
if (strict != null) cmd.apiStrict = strict;
503+
if (deprecationErrors != null) cmd.apiDeprecationErrors = deprecationErrors;
503504
}
504505

505506
if (hasSessionSupport(this) && session) {
@@ -511,7 +512,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
511512
clusterTime = session.clusterTime;
512513
}
513514

514-
const err = applySession(session, finalCmd, options);
515+
const err = applySession(session, cmd, options);
515516
if (err) {
516517
return callback(err);
517518
}
@@ -521,12 +522,12 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
521522

522523
// if we have a known cluster time, gossip it
523524
if (clusterTime) {
524-
finalCmd.$clusterTime = clusterTime;
525+
cmd.$clusterTime = clusterTime;
525526
}
526527

527528
if (isSharded(this) && !shouldUseOpMsg && readPreference && readPreference.mode !== 'primary') {
528-
finalCmd = {
529-
$query: finalCmd,
529+
cmd = {
530+
$query: cmd,
530531
$readPreference: readPreference.toJSON()
531532
};
532533
}
@@ -544,8 +545,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
544545

545546
const cmdNs = `${ns.db}.$cmd`;
546547
const message = shouldUseOpMsg
547-
? new Msg(cmdNs, finalCmd, commandOptions)
548-
: new Query(cmdNs, finalCmd, commandOptions);
548+
? new Msg(cmdNs, cmd, commandOptions)
549+
: new Query(cmdNs, cmd, commandOptions);
549550

550551
try {
551552
write(this, message, commandOptions, callback);

src/cmap/wire_protocol/shared.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Document } from '../../bson';
21
import { MongoInvalidArgumentError } from '../../error';
32
import type { ReadPreferenceLike } from '../../read_preference';
43
import { ReadPreference } from '../../read_preference';
@@ -13,9 +12,9 @@ export interface ReadPreferenceOption {
1312
readPreference?: ReadPreferenceLike;
1413
}
1514

16-
export function getReadPreference(cmd: Document, options?: ReadPreferenceOption): ReadPreference {
15+
export function getReadPreference(options?: ReadPreferenceOption): ReadPreference {
1716
// Default to command version of the readPreference
18-
let readPreference = cmd.readPreference || ReadPreference.primary;
17+
let readPreference = options?.readPreference ?? ReadPreference.primary;
1918
// If we have an option readPreference override the command one
2019
if (options?.readPreference) {
2120
readPreference = options.readPreference;

src/db.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,22 @@ export class Db {
232232
* @remarks
233233
* This command does not inherit options from the MongoClient.
234234
*
235+
* The driver will ensure the following fields are attached to the command sent to the server:
236+
* - `lsid` - sourced from an implicit session or options.session
237+
* - `$readPreference` - defaults to primary or can be configured by options.readPreference
238+
* - `$db` - sourced from the name of this database
239+
*
240+
* If the client has a serverApi setting:
241+
* - `apiVersion`
242+
* - `apiStrict`
243+
* - `apiDeprecationErrors`
244+
*
245+
* When in a transaction:
246+
* - `readConcern` - sourced from readConcern set on the TransactionOptions
247+
* - `writeConcern` - sourced from writeConcern set on the TransactionOptions
248+
*
249+
* Attaching any of the above fields to the command will have no effect as the driver will overwrite the value.
250+
*
235251
* @param command - The command to run
236252
* @param options - Optional settings for the command
237253
*/

src/operations/run_command.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,48 @@
1-
import type { Document } from '../bson';
1+
import type { BSONSerializeOptions, Document } from '../bson';
2+
import type { ReadPreferenceLike } from '../read_preference';
23
import type { Server } from '../sdam/server';
34
import type { ClientSession } from '../sessions';
45
import { Callback, MongoDBNamespace } from '../utils';
5-
import { CommandOperation, CommandOperationOptions, OperationParent } from './command';
6+
import { CommandOperation, OperationParent } from './command';
67

78
/** @public */
8-
export type RunCommandOptions = CommandOperationOptions;
9+
export type RunCommandOptions = {
10+
/** Specify ClientSession for this command */
11+
session?: ClientSession;
12+
/** The read preference */
13+
readPreference?: ReadPreferenceLike;
14+
15+
// The following options were "accidentally" supported
16+
// Since the options are generally supported through inheritance
17+
18+
/** @deprecated This is an internal option that has undefined behavior for this API */
19+
willRetryWrite?: any;
20+
/** @deprecated This is an internal option that has undefined behavior for this API */
21+
omitReadPreference?: any;
22+
/** @deprecated This is an internal option that has undefined behavior for this API */
23+
writeConcern?: any;
24+
/** @deprecated This is an internal option that has undefined behavior for this API */
25+
explain?: any;
26+
/** @deprecated This is an internal option that has undefined behavior for this API */
27+
readConcern?: any;
28+
/** @deprecated This is an internal option that has undefined behavior for this API */
29+
collation?: any;
30+
/** @deprecated This is an internal option that has undefined behavior for this API */
31+
maxTimeMS?: any;
32+
/** @deprecated This is an internal option that has undefined behavior for this API */
33+
comment?: any;
34+
/** @deprecated This is an internal option that has undefined behavior for this API */
35+
retryWrites?: any;
36+
/** @deprecated This is an internal option that has undefined behavior for this API */
37+
dbName?: any;
38+
/** @deprecated This is an internal option that has undefined behavior for this API */
39+
authdb?: any;
40+
/** @deprecated This is an internal option that has undefined behavior for this API */
41+
noResponse?: any;
42+
43+
/** @internal Used for transaction commands */
44+
bypassPinningCheck?: boolean;
45+
} & BSONSerializeOptions;
946

1047
/** @internal */
1148
export class RunCommandOperation<T = Document> extends CommandOperation<T> {

test/integration/run-command/.gitkeep

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { loadSpecTests } from '../../spec';
2+
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
3+
4+
describe('RunCommand spec', () => {
5+
runUnifiedSuite(loadSpecTests('run-command'));
6+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { expect } from 'chai';
2+
3+
import {
4+
CommandStartedEvent,
5+
Db,
6+
MongoClient,
7+
ReadConcern,
8+
ReadPreference,
9+
WriteConcern
10+
} from '../../mongodb';
11+
12+
describe('RunCommand API', () => {
13+
let client: MongoClient;
14+
let db: Db;
15+
let commandsStarted: CommandStartedEvent[];
16+
beforeEach(async function () {
17+
const options = {
18+
serverApi: { version: '1', strict: true, deprecationErrors: false },
19+
monitorCommands: true
20+
};
21+
client = this.configuration.newClient({}, options);
22+
db = client.db();
23+
commandsStarted = [];
24+
client.on('commandStarted', started => commandsStarted.push(started));
25+
});
26+
27+
afterEach(async function () {
28+
commandsStarted = [];
29+
await client.close();
30+
});
31+
32+
it('does not modify user input', { requires: { mongodb: '>=5.0' } }, async () => {
33+
const command = Object.freeze({ ping: 1 });
34+
// will throw if it tries to modify command
35+
await db.command(command, { readPreference: ReadPreference.nearest });
36+
});
37+
38+
it('does not support writeConcern in options', { requires: { mongodb: '>=5.0' } }, async () => {
39+
const command = Object.freeze({ insert: 'test', documents: [{ x: 1 }] });
40+
await db.command(command, { writeConcern: new WriteConcern('majority') });
41+
expect(commandsStarted).to.not.have.nested.property('[0].command.writeConcern');
42+
expect(command).to.not.have.property('writeConcern');
43+
});
44+
45+
// TODO(NODE-4936): We do support readConcern in options, the spec forbids this
46+
it.skip(
47+
'does not support readConcern in options',
48+
{ requires: { mongodb: '>=5.0' } },
49+
async () => {
50+
const command = Object.freeze({ find: 'test', filter: {} });
51+
const res = await db.command(command, { readConcern: ReadConcern.MAJORITY });
52+
expect(res).to.have.property('ok', 1);
53+
expect(commandsStarted).to.not.have.nested.property('[0].command.readConcern');
54+
expect(command).to.not.have.property('readConcern');
55+
}
56+
).skipReason =
57+
'TODO(NODE-4936): Enable this test when readConcern support has been removed from runCommand';
58+
});

0 commit comments

Comments
 (0)