diff --git a/index.js b/index.js index 4e9e6359e86..ec572201bf1 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,9 @@ connect.MongoWriteConcernError = core.MongoWriteConcernError; connect.MongoBulkWriteError = require('./lib/bulk/common').BulkWriteError; connect.BulkWriteError = connect.MongoBulkWriteError; +// Expose server versions +connect.ServerApiVersion = core.ServerApiVersion; + // Actual driver classes exported connect.Admin = require('./lib/admin'); connect.MongoClient = require('./lib/mongo_client'); diff --git a/lib/core/index.js b/lib/core/index.js index cf3bbfed7b2..f1352c2e66e 100644 --- a/lib/core/index.js +++ b/lib/core/index.js @@ -14,7 +14,14 @@ try { } } catch (err) {} // eslint-disable-line +/** An enumeration of valid server API versions */ +const ServerApiVersion = Object.freeze({ + v1: '1' +}); + module.exports = { + // Versioned API + ServerApiVersion, // Errors MongoError: require('./error').MongoError, MongoNetworkError: require('./error').MongoNetworkError, diff --git a/lib/mongo_client.js b/lib/mongo_client.js index 61398aee536..7c65e0d37fc 100644 --- a/lib/mongo_client.js +++ b/lib/mongo_client.js @@ -5,6 +5,7 @@ const Db = require('./db'); const EventEmitter = require('events').EventEmitter; const inherits = require('util').inherits; const MongoError = require('./core').MongoError; +const ServerApiVersion = require('./core').ServerApiVersion; const deprecate = require('util').deprecate; const WriteConcern = require('./write_concern'); const MongoDBNamespace = require('./utils').MongoDBNamespace; @@ -192,16 +193,38 @@ const validOptions = require('./operations/connect').validOptions; * @param {MongoClientOptions} [options] Optional settings */ function MongoClient(url, options) { + options = options || {}; if (!(this instanceof MongoClient)) return new MongoClient(url, options); // Set up event emitter EventEmitter.call(this); - if (options && options.autoEncryption) require('./encrypter'); // Does CSFLE lib check + if (options.autoEncryption) require('./encrypter'); // Does CSFLE lib check + + if (options.serverApi) { + const serverApiToValidate = + typeof options.serverApi === 'string' ? { version: options.serverApi } : options.serverApi; + const versionToValidate = serverApiToValidate && serverApiToValidate.version; + if (!versionToValidate) { + throw new MongoError( + `Invalid \`serverApi\` property; must specify a version from the following enum: ["${Object.values( + ServerApiVersion + ).join('", "')}"]` + ); + } + if (!Object.values(ServerApiVersion).some(v => v === versionToValidate)) { + throw new MongoError( + `Invalid server API version=${versionToValidate}; must be in the following enum: ["${Object.values( + ServerApiVersion + ).join('", "')}"]` + ); + } + options.serverApi = serverApiToValidate; + } // The internal state this.s = { - url: url, - options: options || {}, + url, + options, promiseLibrary: (options && options.promiseLibrary) || Promise, dbCache: new Map(), sessions: new Set(), diff --git a/test/functional/versioned-api.test.js b/test/functional/versioned-api.test.js index 8e73a51bb09..523bded7f21 100644 --- a/test/functional/versioned-api.test.js +++ b/test/functional/versioned-api.test.js @@ -3,17 +3,80 @@ const expect = require('chai').expect; const loadSpecTests = require('../spec/index').loadSpecTests; const runUnifiedTest = require('./unified-spec-runner/runner').runUnifiedTest; +const ServerApiVersion = require('../../lib/core').ServerApiVersion; describe('Versioned API', function() { - it('should throw an error if serverApi version is provided via the uri with new parser', { - metadata: { topology: 'single' }, - test: function(done) { + describe('client option validation', function() { + it('is supported as a client option when it is a valid ServerApiVersion string', function() { + const validVersions = Object.values(ServerApiVersion); + expect(validVersions.length).to.be.at.least(1); + for (const version of validVersions) { + const client = this.configuration.newClient('mongodb://localhost/', { + serverApi: version + }); + expect(client.s.options) + .to.have.property('serverApi') + .deep.equal({ version }); + } + }); + + it('is supported as a client option when it is an object with a valid version property', function() { + const validVersions = Object.values(ServerApiVersion); + expect(validVersions.length).to.be.at.least(1); + for (const version of validVersions) { + const client = this.configuration.newClient('mongodb://localhost/', { + serverApi: { version } + }); + expect(client.s.options) + .to.have.property('serverApi') + .deep.equal({ version }); + } + }); + + it('is not supported as a client option when it is an invalid string', function() { + expect(() => + this.configuration.newClient('mongodb://localhost/', { + serverApi: 'bad' + }) + ).to.throw(/^Invalid server API version=bad;/); + }); + + it('is not supported as a client option when it is a number', function() { + expect(() => + this.configuration.newClient('mongodb://localhost/', { + serverApi: 1 + }) + ).to.throw(/^Invalid `serverApi` property;/); + }); + + it('is not supported as a client option when it is an object without a specified version', function() { + expect(() => + this.configuration.newClient('mongodb://localhost/', { + serverApi: {} + }) + ).to.throw(/^Invalid `serverApi` property;/); + }); + + it('is not supported as a client option when it is an object with an invalid specified version', function() { + expect(() => + this.configuration.newClient('mongodb://localhost/', { + serverApi: { version: 1 } + }) + ).to.throw(/^Invalid server API version=1;/); + expect(() => + this.configuration.newClient('mongodb://localhost/', { + serverApi: { version: 'bad' } + }) + ).to.throw(/^Invalid server API version=bad;/); + }); + + it('is not supported as a URI option even when it is a valid ServerApiVersion string', function(done) { const client = this.configuration.newClient({ serverApi: '1' }, { useNewUrlParser: true }); client.connect(err => { expect(err).to.match(/URI cannot contain `serverApi`, it can only be passed to the client/); client.close(done); }); - } + }); }); for (const versionedApiTest of loadSpecTests('versioned-api')) { diff --git a/test/tools/cluster_setup.sh b/test/tools/cluster_setup.sh index 2f12d17f941..36beca5dc9f 100755 --- a/test/tools/cluster_setup.sh +++ b/test/tools/cluster_setup.sh @@ -2,17 +2,31 @@ if [ "$#" -ne 1 ]; then echo "usage: cluster_setup " + echo "override env variables to change dbPath" exit fi +DATA_DIR=${DATA_DIR:-data} +SINGLE_DIR=${SINGLE_DIR:-$DATA_DIR/server} +REPLICASET_DIR=${REPLICASET_DIR:-$DATA_DIR/replica_set} +SHARDED_DIR=${SHARDED_DIR:-$DATA_DIR/sharded_cluster} + +if [[ ! -z "$MONGODB_API_VERSION" ]]; then + echo "Requiring versioned API $MONGODB_API_VERSION" + REQUIRE_API="--setParameter requireApiVersion=$MONGODB_API_VERSION" +fi + if [[ $1 == "replica_set" ]]; then - mlaunch init --replicaset --nodes 3 --arbiter --name rs --port 31000 --enableMajorityReadConcern --setParameter enableTestCommands=1 - echo "mongodb://localhost:31000/?replicaSet=rs" + mkdir -p $REPLICASET_DIR + mlaunch init --dir $REPLICASET_DIR --replicaset --nodes 3 --arbiter --name rs --port 31000 --enableMajorityReadConcern --setParameter enableTestCommands=1 + echo "mongodb://localhost:31000,localhost:31001,localhost:31002/?replicaSet=rs" elif [[ $1 == "sharded_cluster" ]]; then - mlaunch init --replicaset --nodes 3 --arbiter --name rs --port 51000 --enableMajorityReadConcern --setParameter enableTestCommands=1 --sharded 1 --mongos 2 - echo "mongodb://localhost:51000,localhost:51001/" + mkdir -p $SHARDED_DIR + mlaunch init --dir $SHARDED_DIR --replicaset --nodes 3 --arbiter --name rs --port 51000 --enableMajorityReadConcern --setParameter enableTestCommands=1 --sharded 1 --mongos 2 + echo "mongodb://localhost:51000,localhost:51001" elif [[ $1 == "server" ]]; then - mlaunch init --single --setParameter enableTestCommands=1 + mkdir -p $SINGLE_DIR + mlaunch init --dir $SINGLE_DIR --single --setParameter enableTestCommands=1 $REQUIRE_API echo "mongodb://localhost:27017" else echo "unsupported topology: $1"