Skip to content

Commit d7354c2

Browse files
emadumljhaywar
authored andcommitted
feat: versioned api (#2736)
Allows users to select an API version when connecting to a MongoDB instance. NODE-2950
1 parent 07d7388 commit d7354c2

37 files changed

+3957
-90
lines changed

.evergreen/config.yml

+24-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ functions:
8181
MONGODB_VERSION=${VERSION} TOPOLOGY=${TOPOLOGY} \
8282
AUTH=${AUTH} SSL=${SSL} \
8383
ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \
84+
REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \
8485
bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh
8586
- command: expansions.update
8687
params:
@@ -125,8 +126,10 @@ functions:
125126
rm -f ./prepare_client_encryption.sh
126127
fi
127128
128-
AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} MONGODB_URI="${MONGODB_URI}" \
129-
NODE_VERSION=${NODE_VERSION} SKIP_DEPS=1 NO_EXIT=1 \
129+
MONGODB_URI="${MONGODB_URI}" \
130+
AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \
131+
MONGODB_API_VERSION="${MONGODB_API_VERSION}" \
132+
NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \
130133
bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh
131134
run checks:
132135
- command: shell.exec
@@ -780,6 +783,22 @@ tasks:
780783
VERSION: '2.6'
781784
TOPOLOGY: sharded_cluster
782785
- func: run tests
786+
- name: test-latest-server-v1-api
787+
tags:
788+
- latest
789+
- server
790+
- v1-api
791+
commands:
792+
- func: install dependencies
793+
- func: bootstrap mongo-orchestration
794+
vars:
795+
VERSION: latest
796+
TOPOLOGY: server
797+
REQUIRE_API_VERSION: '1'
798+
- func: run tests
799+
vars:
800+
MONGODB_API_VERSION: '1'
801+
NO_EXIT: ''
783802
- name: test-atlas-connectivity
784803
tags:
785804
- atlas-connect
@@ -1192,6 +1211,7 @@ buildvariants:
11921211
- test-2.6-server
11931212
- test-2.6-replica_set
11941213
- test-2.6-sharded_cluster
1214+
- test-latest-server-v1-api
11951215
- test-atlas-connectivity
11961216
- test-atlas-data-lake
11971217
- test-auth-kerberos
@@ -1258,6 +1278,7 @@ buildvariants:
12581278
- test-2.6-server
12591279
- test-2.6-replica_set
12601280
- test-2.6-sharded_cluster
1281+
- test-latest-server-v1-api
12611282
- test-atlas-connectivity
12621283
- test-atlas-data-lake
12631284
- test-auth-kerberos
@@ -1353,6 +1374,7 @@ buildvariants:
13531374
- test-3.2-server
13541375
- test-3.2-replica_set
13551376
- test-3.2-sharded_cluster
1377+
- test-latest-server-v1-api
13561378
- test-atlas-connectivity
13571379
- test-atlas-data-lake
13581380
- test-auth-kerberos

.evergreen/config.yml.in

+5-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ functions:
9898
MONGODB_VERSION=${VERSION} TOPOLOGY=${TOPOLOGY} \
9999
AUTH=${AUTH} SSL=${SSL} \
100100
ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \
101+
REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \
101102
bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh
102103
# run-orchestration generates expansion file with the MONGODB_URI for the cluster
103104
- command: expansions.update
@@ -145,8 +146,10 @@ functions:
145146
rm -f ./prepare_client_encryption.sh
146147
fi
147148

148-
AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} MONGODB_URI="${MONGODB_URI}" \
149-
NODE_VERSION=${NODE_VERSION} SKIP_DEPS=1 NO_EXIT=1 \
149+
MONGODB_URI="${MONGODB_URI}" \
150+
AUTH=${AUTH} SSL=${SSL} UNIFIED=${UNIFIED} \
151+
MONGODB_API_VERSION="${MONGODB_API_VERSION}" \
152+
NODE_VERSION=${NODE_VERSION} SKIP_DEPS=${SKIP_DEPS|1} NO_EXIT=${NO_EXIT|1} \
150153
bash ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh
151154

152155
"run checks":

.evergreen/generate_evergreen_tasks.js

+23
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const OPERATING_SYSTEMS = [
6060
)
6161
);
6262

63+
// TODO: NODE-3060: enable skipped tests on windows
6364
const WINDOWS_SKIP_TAGS = new Set(['atlas-connect', 'auth']);
6465

6566
const TASKS = [];
@@ -87,6 +88,28 @@ const BASE_TASKS = [];
8788
MONGODB_VERSIONS.forEach(mongoVersion => {
8889
TOPOLOGIES.forEach(topology => BASE_TASKS.push(makeTask({ mongoVersion, topology })));
8990
});
91+
BASE_TASKS.push({
92+
name: `test-latest-server-v1-api`,
93+
tags: ['latest', 'server', 'v1-api'],
94+
commands: [
95+
{ func: 'install dependencies' },
96+
{
97+
func: 'bootstrap mongo-orchestration',
98+
vars: {
99+
VERSION: 'latest',
100+
TOPOLOGY: 'server',
101+
REQUIRE_API_VERSION: '1'
102+
}
103+
},
104+
{
105+
func: 'run tests',
106+
vars: {
107+
MONGODB_API_VERSION: '1',
108+
NO_EXIT: ''
109+
}
110+
}
111+
]
112+
});
90113

91114
// manually added tasks
92115
Array.prototype.push.apply(TASKS, [

.evergreen/run-tests.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ else
5656
npm install mongodb-client-encryption@">=1.2.1"
5757
fi
5858

59-
MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT}
59+
MONGODB_API_VERSION=${MONGODB_API_VERSION} MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT}

src/cmap/auth/scram.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as crypto from 'crypto';
22
import { Binary, Document } from '../../bson';
33
import { MongoError, AnyError } from '../../error';
44
import { AuthProvider, AuthContext } from './auth_provider';
5-
import { Callback, emitWarning, ns } from '../../utils';
5+
import { Callback, ns, emitWarning } from '../../utils';
66
import type { MongoCredentials } from './mongo_credentials';
77
import type { HandshakeDocument } from '../connect';
88

src/cmap/connection.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { applyCommonQueryOptions, getReadPreference, isSharded } from './wire_pr
3838
import { ReadPreference, ReadPreferenceLike } from '../read_preference';
3939
import { isTransactionCommand } from '../transactions';
4040
import type { W, WriteConcern, WriteConcernOptions } from '../write_concern';
41-
import type { SupportedNodeConnectionOptions } from '../mongo_client';
41+
import type { ServerApi, SupportedNodeConnectionOptions } from '../mongo_client';
4242

4343
const kStream = Symbol('stream');
4444
const kQueue = Symbol('queue');
@@ -107,6 +107,7 @@ export interface ConnectionOptions
107107
hostAddress: HostAddress;
108108
// Settings
109109
autoEncrypter?: AutoEncrypter;
110+
serverApi?: ServerApi;
110111
monitorCommands: boolean;
111112
connectionType: typeof Connection;
112113
credentials?: MongoCredentials;
@@ -136,6 +137,7 @@ export class Connection extends EventEmitter {
136137
closed: boolean;
137138
destroyed: boolean;
138139
lastIsMasterMS?: number;
140+
serverApi?: ServerApi;
139141
/** @internal */
140142
[kDescription]: StreamDescription;
141143
/** @internal */
@@ -168,6 +170,7 @@ export class Connection extends EventEmitter {
168170
this.address = streamIdentifier(stream);
169171
this.socketTimeout = options.socketTimeout ?? 0;
170172
this.monitorCommands = options.monitorCommands;
173+
this.serverApi = options.serverApi;
171174
this.closed = false;
172175
this.destroyed = false;
173176

@@ -317,6 +320,15 @@ export class Connection extends EventEmitter {
317320

318321
let clusterTime = this.clusterTime;
319322
let finalCmd = Object.assign({}, cmd);
323+
const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd));
324+
325+
if (this.serverApi && supportsVersionedApi(cmd, session)) {
326+
const { version, strict, deprecationErrors } = this.serverApi;
327+
finalCmd.apiVersion = version;
328+
if (strict != null) finalCmd.apiStrict = strict;
329+
if (deprecationErrors != null) finalCmd.apiDeprecationErrors = deprecationErrors;
330+
}
331+
320332
if (hasSessionSupport(this) && session) {
321333
if (
322334
session.clusterTime &&
@@ -361,7 +373,6 @@ export class Connection extends EventEmitter {
361373
? new Msg(cmdNs, finalCmd, commandOptions)
362374
: new Query(cmdNs, finalCmd, commandOptions);
363375

364-
const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd));
365376
const commandResponseHandler = inTransaction
366377
? (err?: AnyError, ...args: Document[]) => {
367378
// We need to add a TransientTransactionError errorLabel, as stated in the transaction spec.
@@ -630,6 +641,16 @@ function supportsOpMsg(conn: Connection) {
630641
return maxWireVersion(conn) >= 6 && !description.__nodejs_mock_server__;
631642
}
632643

644+
function supportsVersionedApi(cmd: Document, session?: ClientSession) {
645+
const inTransaction = session && (session.inTransaction() || isTransactionCommand(cmd));
646+
// if an API version was declared, add the apiVersion option to every command, except:
647+
// a. only in the initial command of a transaction
648+
// b. only in a Cursor's initiating command, not subsequent getMore commands
649+
return (
650+
(!inTransaction || session?.transaction.isStarting) && !cmd.commitTransaction && !cmd.getMore
651+
);
652+
}
653+
633654
function messageHandler(conn: Connection) {
634655
return function messageHandler(message: BinMsg | Response) {
635656
// always emit the message, in case we are streaming

src/connection_string.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
MongoClient,
2323
MongoClientOptions,
2424
MongoOptions,
25-
PkFactory
25+
PkFactory,
26+
ServerApi
2627
} from './mongo_client';
2728
import { MongoCredentials } from './cmap/auth/mongo_credentials';
2829
import type { TagSet } from './sdam/server_description';
@@ -323,6 +324,12 @@ export function parseOptions(
323324
throw new MongoParseError('URI cannot contain options with no value');
324325
}
325326

327+
if (key.toLowerCase() === 'serverapi') {
328+
throw new MongoParseError(
329+
'URI cannot contain `serverApi`, it can only be passed to the client'
330+
);
331+
}
332+
326333
if (key.toLowerCase() === 'authsource' && urlOptions.has('authSource')) {
327334
// If authSource is an explicit key in the urlOptions we need to remove the implicit dbName
328335
urlOptions.delete('authSource');
@@ -572,6 +579,15 @@ export const OPTIONS = {
572579
autoEncryption: {
573580
type: 'record'
574581
},
582+
serverApi: {
583+
target: 'serverApi',
584+
transform({ values: [version] }): ServerApi {
585+
if (typeof version === 'string') {
586+
return { version };
587+
}
588+
return version as ServerApi;
589+
}
590+
},
575591
checkKeys: {
576592
type: 'boolean'
577593
},

src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export { Compressor } from './cmap/wire_protocol/compression';
8585
export { ExplainVerbosity } from './explain';
8686
export { ReadConcernLevel } from './read_concern';
8787
export { ReadPreferenceMode } from './read_preference';
88+
export { ServerApiVersion } from './mongo_client';
8889

8990
// events
9091
export {
@@ -211,6 +212,8 @@ export type {
211212
Auth,
212213
DriverInfo,
213214
MongoOptions,
215+
ServerApi,
216+
ServerApiVersionId,
214217
SupportedNodeConnectionOptions,
215218
SupportedTLSConnectionOptions,
216219
SupportedTLSSocketOptions,

src/mongo_client.ts

+22
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ import type { SrvPoller } from './sdam/srv_polling';
3232
import type { Connection } from './cmap/connection';
3333
import type { LEGAL_TLS_SOCKET_OPTIONS, LEGAL_TCP_SOCKET_OPTIONS } from './cmap/connect';
3434

35+
/** @public */
36+
export const ServerApiVersion = {
37+
v1: '1'
38+
};
39+
40+
/** @public */
41+
export type ServerApiVersionId = typeof ServerApiVersion[keyof typeof ServerApiVersion];
42+
43+
/** @public */
44+
export interface ServerApi {
45+
version: string | ServerApiVersionId;
46+
strict?: boolean;
47+
deprecationErrors?: boolean;
48+
}
49+
3550
/** @public */
3651
export interface DriverInfo {
3752
name?: string;
@@ -198,6 +213,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
198213
logger?: Logger;
199214
/** Enable command monitoring for this client */
200215
monitorCommands?: boolean;
216+
/** Server API version */
217+
serverApi?: ServerApi | ServerApiVersionId;
201218
/** Optionally enable client side auto encryption */
202219
autoEncryption?: AutoEncryptionOptions;
203220
/** Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver */
@@ -306,6 +323,10 @@ export class MongoClient extends EventEmitter {
306323
return Object.freeze({ ...this[kOptions] });
307324
}
308325

326+
get serverApi(): Readonly<ServerApi | undefined> {
327+
return this[kOptions].serverApi && Object.freeze({ ...this[kOptions].serverApi });
328+
}
329+
309330
get autoEncrypter(): AutoEncrypter | undefined {
310331
return this[kOptions].autoEncrypter;
311332
}
@@ -597,6 +618,7 @@ export interface MongoOptions
597618
credentials?: MongoCredentials;
598619
readPreference: ReadPreference;
599620
readConcern: ReadConcern;
621+
serverApi: ServerApi;
600622
writeConcern: WriteConcern;
601623
dbName: string;
602624
metadata: ClientMetadata;

src/operations/update.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export function makeUpdateStatement(
274274
op.upsert = options.upsert;
275275
}
276276

277-
if (typeof options.multi === 'boolean') {
277+
if (options.multi) {
278278
op.multi = options.multi;
279279
}
280280

src/sdam/server.ts

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type { ServerHeartbeatSucceededEvent } from './events';
4444
import type { ClientSession } from '../sessions';
4545
import type { Document, Long } from '../bson';
4646
import type { AutoEncrypter } from '../deps';
47+
import type { ServerApi } from '../mongo_client';
4748

4849
// Used for filtering out fields for logging
4950
const DEBUG_FIELDS = [
@@ -100,12 +101,15 @@ export interface ServerPrivate {
100101
topology: Topology;
101102
/** A connection pool for this server */
102103
pool: ConnectionPool;
104+
/** MongoDB server API version */
105+
serverApi?: ServerApi;
103106
}
104107

105108
/** @public */
106109
export class Server extends EventEmitter {
107110
/** @internal */
108111
s: ServerPrivate;
112+
serverApi?: ServerApi;
109113
clusterTime?: ClusterTime;
110114
ismaster?: Document;
111115
[kMonitor]: Monitor;
@@ -131,6 +135,8 @@ export class Server extends EventEmitter {
131135
constructor(topology: Topology, description: ServerDescription, options: ServerOptions) {
132136
super();
133137

138+
this.serverApi = options.serverApi;
139+
134140
const poolOptions = { hostAddress: description.hostAddress, ...options };
135141

136142
this.s = {

src/sdam/topology.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import type { MongoCredentials } from '../cmap/auth/mongo_credentials';
5050
import type { Transaction } from '../transactions';
5151
import type { CloseOptions } from '../cmap/connection_pool';
5252
import { DestroyOptions, Connection } from '../cmap/connection';
53-
import type { MongoClientOptions } from '../mongo_client';
53+
import type { MongoClientOptions, ServerApi } from '../mongo_client';
5454
import { DEFAULT_OPTIONS } from '../connection_string';
5555
import { serialize, deserialize } from '../bson';
5656

@@ -139,6 +139,8 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
139139
/** Indicates that a client should directly connect to a node without attempting to discover its topology type */
140140
directConnection: boolean;
141141
metadata: ClientMetadata;
142+
/** MongoDB server API version */
143+
serverApi?: ServerApi;
142144
}
143145

144146
/** @public */

src/transactions.ts

+5
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export class Transaction {
116116
return !!this.server;
117117
}
118118

119+
/** @returns Whether the transaction has started */
120+
get isStarting(): boolean {
121+
return this.state === TxnState.STARTING_TRANSACTION;
122+
}
123+
119124
/**
120125
* @returns Whether this session is presently in a transaction
121126
*/

0 commit comments

Comments
 (0)