This commit is contained in:
2020-08-20 11:44:32 +02:00
parent 4715fc1814
commit 6aceefeb2f
2891 changed files with 11239 additions and 347539 deletions

142
node_modules/mongodb/HISTORY.md generated vendored
View File

@@ -1,38 +1,131 @@
# Change Log
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
<a name="3.5.9"></a>
## [3.5.9](https://github.com/mongodb/node-mongodb-native/compare/v3.5.8...v3.5.9) (2020-06-12)
### Bug Fixes
* don't try to calculate sMax if there are no viable servers ([be51347](https://github.com/mongodb/node-mongodb-native/commit/be51347))
* use async interruptable interval for server monitoring ([1f855a4](https://github.com/mongodb/node-mongodb-native/commit/1f855a4))
* use duration of handshake if no previous roundTripTime exists ([ddfa41b](https://github.com/mongodb/node-mongodb-native/commit/ddfa41b))
## [3.6.0](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.6.0) (2020-07-30)
### Features
* introduce an interruptable async interval timer ([9e12cd5](https://github.com/mongodb/node-mongodb-native/commit/9e12cd5))
<a name="3.5.8"></a>
## [3.5.8](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.5.8) (2020-05-28)
* add commitQuorum option to createIndexes command ([38bcaf7](https://github.com/mongodb/node-mongodb-native/commit/38bcaf7c80f63885d4c0cf1f7389819efb0664e6))
* introduce an interruptable async interval timer ([1e4e837](https://github.com/mongodb/node-mongodb-native/commit/1e4e8373308a6d99c4d7eda4ae395f8293de98eb))
* support streaming ismaster responses ([f6629d9](https://github.com/mongodb/node-mongodb-native/commit/f6629d9946d6d02012a6f1f7848306bc71fb3254))
* **reIndex:** deprecate and make standalone-only command ([3a53b3d](https://github.com/mongodb/node-mongodb-native/commit/3a53b3deb7b375f6ce1e1a7435706aa4df7b0a0c))
* add helper to run async collection in series ([f762532](https://github.com/mongodb/node-mongodb-native/commit/f76253242a9b9c3d1fc5be215e8b677af2138772))
* allow hinting the delete command ([84cf955](https://github.com/mongodb/node-mongodb-native/commit/84cf95519d55e478b6457ba2347daf448388caf2))
* consider staleness and topologyVersion in error handling ([018e6ed](https://github.com/mongodb/node-mongodb-native/commit/018e6edc923f1bb7aa13bf376edd71172bfc5039))
* introduce `MongoNetworkTimeoutError` ([c1e4477](https://github.com/mongodb/node-mongodb-native/commit/c1e44777f9ede629290bb776c420018a4acdc487))
* support checking if network error happened before handshake ([5411786](https://github.com/mongodb/node-mongodb-native/commit/541178690d558a4484d43d2a090d0b4d5416c031))
* support hedged reads ([37cd5ee](https://github.com/mongodb/node-mongodb-native/commit/37cd5eed9f872429f3d0f683ed92cc75231d4685))
* support speculative authentication in scram-sha and x509 ([6231164](https://github.com/mongodb/node-mongodb-native/commit/62311645c5e9a44678e4a997b6a5944b64492bd8))
* **geoHaystackSearch:** deprecate geoHaystackSearch ([3f6786b](https://github.com/mongodb/node-mongodb-native/commit/3f6786b4c00d0650eb9c8009507e9c54b2b11bbc))
* **sdam:** ignore stale topology updates ([f87d243](https://github.com/mongodb/node-mongodb-native/commit/f87d243587c1712cd95624d036ddb9a643a15a84))
### Bug Fixes
* always clear cancelled wait queue members during processing ([0394f9d](https://github.com/mongodb/node-mongodb-native/commit/0394f9d))
* always include `writeErrors` on a `BulkWriteError` instance ([58b4f94](https://github.com/mongodb/node-mongodb-native/commit/58b4f94))
* ensure implicit sessions are ended consistently ([5c6fda1](https://github.com/mongodb/node-mongodb-native/commit/5c6fda1))
* filter servers before applying reducers ([4faf9f5](https://github.com/mongodb/node-mongodb-native/commit/4faf9f5))
* unordered bulk write should attempt to execute all batches ([6cee96b](https://github.com/mongodb/node-mongodb-native/commit/6cee96b))
* **ChangeStream:** should resume from errors when iterating ([5ecf18e](https://github.com/mongodb/node-mongodb-native/commit/5ecf18e))
* honor journal=true in connection string ([#2359](https://github.com/mongodb/node-mongodb-native/issues/2359)) ([246669f](https://github.com/mongodb/node-mongodb-native/commit/246669f))
* **ChangeStream:** whitelist resumable errors ([#2337](https://github.com/mongodb/node-mongodb-native/issues/2337)) ([a9d3965](https://github.com/mongodb/node-mongodb-native/commit/a9d3965)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
* `resolveAuthMechanism` should not mutate credentials ([be19a21](https://github.com/mongodb/node-mongodb-native/commit/be19a2160d33d8e7473800fbcf6af1cd7f6eaeef))
* always clear cancelled wait queue members during processing ([3087d48](https://github.com/mongodb/node-mongodb-native/commit/3087d48c5de17dcd77d48b35af591a8f6780d145))
* always include `writeErrors` on a `BulkWriteError` instance ([40db47f](https://github.com/mongodb/node-mongodb-native/commit/40db47f5aac1e93a11e8568327e93c4c542e11da))
* assert update/replace atomic requirements in bulk operations ([31ae3c9](https://github.com/mongodb/node-mongodb-native/commit/31ae3c9978d6ea4f33c3ea4ccac9cd41841e31de))
* clarify handle wrong set name single topology ([21424eb](https://github.com/mongodb/node-mongodb-native/commit/21424ebb961db339be8fa74babaaeb3ada8e22c8))
* createCollection only uses listCollections in strict mode ([a8ffec4](https://github.com/mongodb/node-mongodb-native/commit/a8ffec4c0b4e2ef0ce4c3bcdadc0adeccd0a3544))
* db.command to not inherit options from parent ([c394284](https://github.com/mongodb/node-mongodb-native/commit/c39428421c94b4a1c5f41340e5b1a82724af95bd))
* don't create multiple rtt pingers if one already exists ([56723aa](https://github.com/mongodb/node-mongodb-native/commit/56723aa1c980894ededd97531ff023a603cf4271))
* don't immediately schedule monitoring after streaming failure ([188c23e](https://github.com/mongodb/node-mongodb-native/commit/188c23e0bb173a032df6bea4c7d2a916b808d9ad))
* don't reapply socket timeout when socket has `moreToCome` ([ca0f2b9](https://github.com/mongodb/node-mongodb-native/commit/ca0f2b9d3fc268187d112e3afbe7bc556273e100))
* don't try to calculate sMax if there are no viable servers ([4cb9b64](https://github.com/mongodb/node-mongodb-native/commit/4cb9b64bc3ff9aed8f26091cfe52acb152146898))
* ensure implicit sessions are ended consistently ([8c861f3](https://github.com/mongodb/node-mongodb-native/commit/8c861f3e6c743a65ee95c8036ee0c1b1bc8c427a))
* filter servers before applying reducers ([6f7d9bf](https://github.com/mongodb/node-mongodb-native/commit/6f7d9bf7f03108af3d190965a8b536ec2fb68a0b))
* hint should raise error on unacknowledged writes ([54aa19e](https://github.com/mongodb/node-mongodb-native/commit/54aa19ea10a18ee6fce93f7c7eb6562c9f4042a5))
* honor journal=true in connection string ([#2358](https://github.com/mongodb/node-mongodb-native/issues/2358)) ([4df4b7c](https://github.com/mongodb/node-mongodb-native/commit/4df4b7c3133925432954ce249c8bf8ae34674cb9))
* IPv6 is not supported when using dns service discovery ([19ec62f](https://github.com/mongodb/node-mongodb-native/commit/19ec62fad5912b5e1f615278a4c2fba153da5030))
* linting issue ([babf845](https://github.com/mongodb/node-mongodb-native/commit/babf84517675240c5ca39631c771e04ba9e248d7))
* MONGODB-AWS temporary credentials are added to authContext ([769a754](https://github.com/mongodb/node-mongodb-native/commit/769a75491ede69a6af8da8f2d6572c6c85e14a62))
* ReadPreference maxStalenessSeconds from options ([dfe7afa](https://github.com/mongodb/node-mongodb-native/commit/dfe7afaf646b2cde1751e728b52fa40d672beb55))
* reduce default keepalive time to align with Azure defaults ([72d8969](https://github.com/mongodb/node-mongodb-native/commit/72d896983ed70fd0f96dea3a37ff05a5d91a93ba))
* remove destructuring assignments for legacy node support ([d728a13](https://github.com/mongodb/node-mongodb-native/commit/d728a1330a37e847b69937b42e179c58b47038cb))
* silently ignore session with unacknowledged write ([a053f4e](https://github.com/mongodb/node-mongodb-native/commit/a053f4ea3f5ad1c8c8a581c449cf03dc252aeb06))
* **ChangeStream:** handle null changes ([14179a2](https://github.com/mongodb/node-mongodb-native/commit/14179a270ce4bfa62884ee53d46f4e35d28b6100))
* **ChangeStream:** make CursorNotFound error resumable ([9f0b7ab](https://github.com/mongodb/node-mongodb-native/commit/9f0b7ab65a5d443f411a61fa1a3dd6cfe42b8dce))
* **ChangeStream:** should resume from errors when iterating ([497952c](https://github.com/mongodb/node-mongodb-native/commit/497952cd577dc692dc2e0273028a74c818d31174))
* **ChangeStream:** whitelist resumable errors ([#2337](https://github.com/mongodb/node-mongodb-native/issues/2337)) ([a9d3965](https://github.com/mongodb/node-mongodb-native/commit/a9d39651e5a3d5e565a85d1eb503f56f07c4a1d3)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
* **create_indexes:** add missing `bucketSize` option to list of valid options ([66c76c3](https://github.com/mongodb/node-mongodb-native/commit/66c76c32f900499e147656e07b2df7bc5e0bb1b5))
* **GridFS:** emit error on bad options ([c71a4df](https://github.com/mongodb/node-mongodb-native/commit/c71a4dfdf1c4904321a2b50967b3379b13d774fb))
* remove check for NonResumableChangeStreamError label ([f3ac635](https://github.com/mongodb/node-mongodb-native/commit/f3ac635663717dd9eb00bf8d3f938a56c98bcafb))
* throw an error if `allowDiskUse` is used on MongoDB < 3.2 ([f95f697](https://github.com/mongodb/node-mongodb-native/commit/f95f697dde5ba1ae4c6a698ad29266dfd787a121))
* typo with setting error labels on error object ([89638bf](https://github.com/mongodb/node-mongodb-native/commit/89638bf925482bb54c3dd956ffd6f2830d088c6a))
* unordered bulk write should attempt to execute all batches ([d00a644](https://github.com/mongodb/node-mongodb-native/commit/d00a644e129ad9570a5727b7e259261204f259ae))
* use async interruptable interval for server monitoring ([068ae83](https://github.com/mongodb/node-mongodb-native/commit/068ae83291fc30f76b23ea1120e77ce71053e29d))
* writes within transactions are not retryable ([c13ec5c](https://github.com/mongodb/node-mongodb-native/commit/c13ec5c66623af164b688c79747eab8fe89b91d6))
## [3.6.0-beta.0](https://github.com/mongodb/node-mongodb-native/compare/v3.5.5...v3.6.0-beta.0) (2020-04-14)
### Features
* add MONGODB-AWS as a supported auth mechanism ([7f3cfba](https://github.com/mongodb/node-mongodb-native/commit/7f3cfbac15f537aa2ca9da145063f10c61390406))
* bump wire protocol version for 4.4 ([6d3f313](https://github.com/mongodb/node-mongodb-native/commit/6d3f313a9defd12489b621896439b3f9ec8cb1ae))
* deprecate `oplogReplay` for find commands ([24155e7](https://github.com/mongodb/node-mongodb-native/commit/24155e7905422460afc7e6abb120c596f40712c1))
* directConnection adds unify behavior for replica set discovery ([c5d60fc](https://github.com/mongodb/node-mongodb-native/commit/c5d60fc4619227697ef2102437fe5c8b111909d2))
* expand use of error labels for retryable writes ([c775a4a](https://github.com/mongodb/node-mongodb-native/commit/c775a4a1c53b8476eff6c9759b5647c9cbfa4e04))
* support `allowDiskUse` for find commands ([dbc0b37](https://github.com/mongodb/node-mongodb-native/commit/dbc0b3722516a128c253bf85366a3432756ff92a))
* support creating collections and indexes in transactions ([17e4c88](https://github.com/mongodb/node-mongodb-native/commit/17e4c88575b734d2d8ff94ca7f68b731a0bad326))
* support passing a hint to findOneAndReplace/findOneAndUpdate ([faee15b](https://github.com/mongodb/node-mongodb-native/commit/faee15b686b895b84fd0b52c1e69e0caec769732))
* support shorter SCRAM conversations ([6b9ff05](https://github.com/mongodb/node-mongodb-native/commit/6b9ff0561d14818bf07f4946ade04fc54683d0b9))
* use error labels for retryable writes in legacy topologies ([fefc165](https://github.com/mongodb/node-mongodb-native/commit/fefc1651a885ec28758271c9e3c36104b05bdb75))
### Bug Fixes
* **ChangeStream:** whitelist change stream resumable errors ([8a9c108](https://github.com/mongodb/node-mongodb-native/commit/8a9c1084430de9d6253ca9c61c9258c85835bb94)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
* always return empty array for selection on unknown topology ([af57b57](https://github.com/mongodb/node-mongodb-native/commit/af57b578dd603faa7b66983232de2bc7e417dae1))
* correctly use template string for connection string error message ([814e278](https://github.com/mongodb/node-mongodb-native/commit/814e27869d90a1dfa01118bb96ff1273e0cef323))
* don't depend on private node api for `Timeout` wrapper ([e6dc1f4](https://github.com/mongodb/node-mongodb-native/commit/e6dc1f48d62b68ba56b93359d7aa755c08985867))
* **sdam:** use ObjectId comparison to track maxElectionId ([db991d6](https://github.com/mongodb/node-mongodb-native/commit/db991d6916306d1fe08508d4c3e8f7a37d7fd21f))
* only consider MongoError subclasses for retryability ([265fe40](https://github.com/mongodb/node-mongodb-native/commit/265fe40cf29992764d1ab030a1ee4dca97cd7c7c))
* pass options into `commandSupportsReadConcern` ([e855c83](https://github.com/mongodb/node-mongodb-native/commit/e855c83d8b73f4ce57a11193a1e52461ab2cd4db))
* store name of collection for more informative error messages ([979d41e](https://github.com/mongodb/node-mongodb-native/commit/979d41e14f5acf69bac094b3863591ee8e01fd9c))
* support write concern provided as string in `fromOptions` ([637f428](https://github.com/mongodb/node-mongodb-native/commit/637f4288c1edb799267ccbce6d25a49304f6149c))
* use properly camel cased form of `mapReduce` for command ([c1ed2c1](https://github.com/mongodb/node-mongodb-native/commit/c1ed2c1ce4c6f2d40cd1c7b84ad672a90a09c83b))
<a name="3.6.0-beta.0"></a>
# [3.6.0-beta.0](https://github.com/mongodb/node-mongodb-native/compare/v3.5.5...v3.6.0-beta.0) (2020-04-14)
### Bug Fixes
* always return empty array for selection on unknown topology ([af57b57](https://github.com/mongodb/node-mongodb-native/commit/af57b57))
* always return empty array for selection on unknown topology ([f9e786a](https://github.com/mongodb/node-mongodb-native/commit/f9e786a))
* correctly use template string for connection string error message ([814e278](https://github.com/mongodb/node-mongodb-native/commit/814e278))
* createCollection only uses listCollections in strict mode ([d368f12](https://github.com/mongodb/node-mongodb-native/commit/d368f12))
* don't depend on private node api for `Timeout` wrapper ([e6dc1f4](https://github.com/mongodb/node-mongodb-native/commit/e6dc1f4))
* don't throw if `withTransaction()` callback rejects with a null reason ([153646c](https://github.com/mongodb/node-mongodb-native/commit/153646c))
* **cursor:** transforms should only be applied once to documents ([704f30a](https://github.com/mongodb/node-mongodb-native/commit/704f30a))
* only consider MongoError subclasses for retryability ([265fe40](https://github.com/mongodb/node-mongodb-native/commit/265fe40))
* **ChangeStream:** whitelist change stream resumable errors ([8a9c108](https://github.com/mongodb/node-mongodb-native/commit/8a9c108)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
* **sdam:** use ObjectId comparison to track maxElectionId ([db991d6](https://github.com/mongodb/node-mongodb-native/commit/db991d6))
* only mark server session dirty if the client session is alive ([611be8d](https://github.com/mongodb/node-mongodb-native/commit/611be8d))
* pass options into `commandSupportsReadConcern` ([e855c83](https://github.com/mongodb/node-mongodb-native/commit/e855c83))
* polyfill for util.promisify ([1c4cf6c](https://github.com/mongodb/node-mongodb-native/commit/1c4cf6c))
* single `readPreferenceTags` should be parsed as an array ([a50611b](https://github.com/mongodb/node-mongodb-native/commit/a50611b))
* store name of collection for more informative error messages ([979d41e](https://github.com/mongodb/node-mongodb-native/commit/979d41e))
* support write concern provided as string in `fromOptions` ([637f428](https://github.com/mongodb/node-mongodb-native/commit/637f428))
* use properly camel cased form of `mapReduce` for command ([c1ed2c1](https://github.com/mongodb/node-mongodb-native/commit/c1ed2c1))
### Features
* add MONGODB-AWS as a supported auth mechanism ([7f3cfba](https://github.com/mongodb/node-mongodb-native/commit/7f3cfba))
* bump wire protocol version for 4.4 ([6d3f313](https://github.com/mongodb/node-mongodb-native/commit/6d3f313))
* deprecate `oplogReplay` for find commands ([24155e7](https://github.com/mongodb/node-mongodb-native/commit/24155e7))
* directConnection adds unify behavior for replica set discovery ([c5d60fc](https://github.com/mongodb/node-mongodb-native/commit/c5d60fc))
* expand use of error labels for retryable writes ([c775a4a](https://github.com/mongodb/node-mongodb-native/commit/c775a4a))
* support `allowDiskUse` for find commands ([dbc0b37](https://github.com/mongodb/node-mongodb-native/commit/dbc0b37))
* support creating collections and indexes in transactions ([17e4c88](https://github.com/mongodb/node-mongodb-native/commit/17e4c88))
* support passing a hint to findOneAndReplace/findOneAndUpdate ([faee15b](https://github.com/mongodb/node-mongodb-native/commit/faee15b))
* support shorter SCRAM conversations ([6b9ff05](https://github.com/mongodb/node-mongodb-native/commit/6b9ff05))
* use error labels for retryable writes in legacy topologies ([fefc165](https://github.com/mongodb/node-mongodb-native/commit/fefc165))
@@ -50,7 +143,6 @@ All notable changes to this project will be documented in this file. See [standa
<a name="3.5.6"></a>
## [3.5.6](https://github.com/mongodb/node-mongodb-native/compare/v3.5.5...v3.5.6) (2020-04-14)
### Bug Fixes
* always return empty array for selection on unknown topology ([f9e786a](https://github.com/mongodb/node-mongodb-native/commit/f9e786a))

1
node_modules/mongodb/lib/admin.js generated vendored
View File

@@ -232,6 +232,7 @@ Admin.prototype.removeUser = function(username, options, callback) {
*
* @param {string} collectionName The name of the collection to validate.
* @param {object} [options] Optional settings.
* @param {boolean} [options.background] Validates a collection in the background, without interrupting read or write traffic (only in MongoDB 4.4+)
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {Admin~resultCallback} [callback] The command result callback.
* @return {Promise} returns Promise if no callback passed

View File

@@ -11,6 +11,8 @@ const applyRetryableWrites = require('../utils').applyRetryableWrites;
const applyWriteConcern = require('../utils').applyWriteConcern;
const executeLegacyOperation = require('../utils').executeLegacyOperation;
const isPromiseLike = require('../utils').isPromiseLike;
const hasAtomicOperators = require('../utils').hasAtomicOperators;
const maxWireVersion = require('../core/utils').maxWireVersion;
// Error codes
const WRITE_CONCERN_ERROR = 64;
@@ -641,6 +643,10 @@ class FindOperators {
document.hint = updateDocument.hint;
}
if (!hasAtomicOperators(updateDocument)) {
throw new TypeError('Update document requires atomic operators');
}
// Clear out current Op
this.s.currentOp = null;
return this.s.options.addToOperationsList(this, UPDATE, document);
@@ -650,12 +656,33 @@ class FindOperators {
* Add a replace one operation to the bulk operation
*
* @method
* @param {object} updateDocument the new document to replace the existing one with
* @param {object} replacement the new document to replace the existing one with
* @throws {MongoError} If operation cannot be added to bulk write
* @return {OrderedBulkOperation|UnorderedBulkOperation} A reference to the parent BulkOperation
*/
replaceOne(updateDocument) {
this.updateOne(updateDocument);
replaceOne(replacement) {
// Perform upsert
const upsert = typeof this.s.currentOp.upsert === 'boolean' ? this.s.currentOp.upsert : false;
// Establish the update command
const document = {
q: this.s.currentOp.selector,
u: replacement,
multi: false,
upsert: upsert
};
if (replacement.hint) {
document.hint = replacement.hint;
}
if (hasAtomicOperators(replacement)) {
throw new TypeError('Replacement document must not use atomic operators');
}
// Clear out current Op
this.s.currentOp = null;
return this.s.options.addToOperationsList(this, UPDATE, document);
}
/**
@@ -943,6 +970,12 @@ class BulkOperationBase {
// Crud spec update format
if (op.updateOne || op.updateMany || op.replaceOne) {
if (op.replaceOne && hasAtomicOperators(op[key].replacement)) {
throw new TypeError('Replacement document must not use atomic operators');
} else if ((op.updateOne || op.updateMany) && !hasAtomicOperators(op[key].update)) {
throw new TypeError('Update document requires atomic operators');
}
const multi = op.updateOne || op.replaceOne ? false : true;
const operation = {
q: op[key].filter,
@@ -960,7 +993,15 @@ class BulkOperationBase {
} else {
if (op[key].upsert) operation.upsert = true;
}
if (op[key].arrayFilters) operation.arrayFilters = op[key].arrayFilters;
if (op[key].arrayFilters) {
// TODO: this check should be done at command construction against a connection, not a topology
if (maxWireVersion(this.s.topology) < 6) {
throw new TypeError('arrayFilters are only supported on MongoDB 3.6+');
}
operation.arrayFilters = op[key].arrayFilters;
}
return this.s.options.addToOperationsList(this, UPDATE, operation);
}
@@ -979,6 +1020,9 @@ class BulkOperationBase {
if (op.deleteOne || op.deleteMany) {
const limit = op.deleteOne ? 1 : 0;
const operation = { q: op[key].filter, limit: limit };
if (op[key].hint) {
operation.hint = op[key].hint;
}
if (this.isOrdered) {
if (op.collation) operation.collation = op.collation;
}

View File

@@ -169,7 +169,6 @@ class ChangeStream extends EventEmitter {
/**
* Is the change stream closed
* @method ChangeStream.prototype.isClosed
* @param {boolean} [checkCursor=true] also check if the underlying cursor is closed
* @return {boolean}
*/
isClosed() {
@@ -326,8 +325,8 @@ class ChangeStreamCursor extends Cursor {
_initializeCursor(callback) {
super._initializeCursor((err, result) => {
if (err) {
callback(err);
if (err || result == null) {
callback(err, result);
return;
}
@@ -483,6 +482,11 @@ function waitForTopologyConnected(topology, options, callback) {
function processNewChange(changeStream, change, callback) {
const cursor = changeStream.cursor;
// a null change means the cursor has been notified, implicitly closing the change stream
if (change == null) {
changeStream.closed = true;
}
if (changeStream.closed) {
if (callback) callback(new MongoError('ChangeStream is closed'));
return;

View File

@@ -4,6 +4,7 @@ const EventEmitter = require('events');
const MessageStream = require('./message_stream');
const MongoError = require('../core/error').MongoError;
const MongoNetworkError = require('../core/error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../core/error').MongoNetworkTimeoutError;
const MongoWriteConcernError = require('../core/error').MongoWriteConcernError;
const CommandResult = require('../core/connection/command_result');
const StreamDescription = require('./stream_description').StreamDescription;
@@ -77,10 +78,14 @@ class Connection extends EventEmitter {
stream.destroy();
this.closed = true;
this[kQueue].forEach(op =>
op.cb(new MongoNetworkError(`connection ${this.id} to ${this.address} timed out`))
op.cb(
new MongoNetworkTimeoutError(`connection ${this.id} to ${this.address} timed out`, {
beforeHandshake: this[kIsMaster] == null
})
)
);
this[kQueue].clear();
this[kQueue].clear();
this.emit('close');
});
@@ -218,6 +223,7 @@ function messageHandler(conn) {
}
const operationDescription = conn[kQueue].get(message.responseTo);
const callback = operationDescription.cb;
// SERVER-45775: For exhaust responses we should be able to use the same requestId to
// track response, however the server currently synthetically produces remote requests
@@ -226,10 +232,7 @@ function messageHandler(conn) {
if (message.moreToCome) {
// requeue the callback for next synthetic request
conn[kQueue].set(message.requestId, operationDescription);
}
const callback = operationDescription.cb;
if (operationDescription.socketTimeoutOverride) {
} else if (operationDescription.socketTimeoutOverride) {
conn[kStream].setTimeout(conn.socketTimeout);
}

View File

@@ -5,7 +5,6 @@ const deprecateOptions = require('./utils').deprecateOptions;
const checkCollectionName = require('./utils').checkCollectionName;
const ObjectID = require('./core').BSON.ObjectID;
const MongoError = require('./core').MongoError;
const toError = require('./utils').toError;
const normalizeHintField = require('./utils').normalizeHintField;
const decorateCommand = require('./utils').decorateCommand;
const decorateWithCollation = require('./utils').decorateWithCollation;
@@ -16,7 +15,6 @@ const unordered = require('./bulk/unordered');
const ordered = require('./bulk/ordered');
const ChangeStream = require('./change_stream');
const executeLegacyOperation = require('./utils').executeLegacyOperation;
const resolveReadPreference = require('./utils').resolveReadPreference;
const WriteConcern = require('./write_concern');
const ReadConcern = require('./read_concern');
const MongoDBNamespace = require('./utils').MongoDBNamespace;
@@ -24,7 +22,6 @@ const AggregationCursor = require('./aggregation_cursor');
const CommandCursor = require('./command_cursor');
// Operations
const checkForAtomicOperators = require('./operations/collection_ops').checkForAtomicOperators;
const ensureIndex = require('./operations/collection_ops').ensureIndex;
const group = require('./operations/collection_ops').group;
const parallelCollectionScan = require('./operations/collection_ops').parallelCollectionScan;
@@ -35,7 +32,6 @@ const updateDocuments = require('./operations/common_functions').updateDocuments
const AggregateOperation = require('./operations/aggregate');
const BulkWriteOperation = require('./operations/bulk_write');
const CountDocumentsOperation = require('./operations/count_documents');
const CreateIndexOperation = require('./operations/create_index');
const CreateIndexesOperation = require('./operations/create_indexes');
const DeleteManyOperation = require('./operations/delete_many');
const DeleteOneOperation = require('./operations/delete_one');
@@ -278,7 +274,7 @@ Object.defineProperty(Collection.prototype, 'hint', {
}
});
const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot'];
const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot', 'oplogReplay'];
/**
* Creates a cursor for a query that can be used to iterate over results from MongoDB
@@ -313,6 +309,7 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot'];
* @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. Requires `tailable` and `awaitData` to be true
* @param {boolean} [options.noCursorTimeout] The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to prevent that.
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {boolean} [options.allowDiskUse] Enables writing to temporary files on the server.
* @param {ClientSession} [options.session] optional session to use for this operation
* @throws {MongoError}
* @return {Cursor}
@@ -399,7 +396,7 @@ Collection.prototype.find = deprecateOptions(
newOptions.slaveOk = options.slaveOk != null ? options.slaveOk : this.s.db.slaveOk;
// Add read preference if needed
newOptions.readPreference = resolveReadPreference(this, newOptions);
newOptions.readPreference = ReadPreference.resolve(this, newOptions);
// Set slave ok to true if read preference different from primary
if (
@@ -422,6 +419,10 @@ Collection.prototype.find = deprecateOptions(
query: selector
};
if (typeof options.allowDiskUse === 'boolean') {
findCommand.allowDiskUse = options.allowDiskUse;
}
// Ensure we use the right await data option
if (typeof newOptions.awaitdata === 'boolean') {
newOptions.awaitData = newOptions.awaitdata;
@@ -744,14 +745,6 @@ Collection.prototype.insert = deprecate(function(docs, options, callback) {
*/
Collection.prototype.updateOne = function(filter, update, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
const err = checkForAtomicOperators(update);
if (err) {
if (typeof callback === 'function') return callback(err);
return this.s.promiseLibrary.reject(err);
}
options = Object.assign({}, options);
// Add ignoreUndefined
@@ -760,9 +753,11 @@ Collection.prototype.updateOne = function(filter, update, options, callback) {
options.ignoreUndefined = this.s.options.ignoreUndefined;
}
const updateOneOperation = new UpdateOneOperation(this, filter, update, options);
return executeOperation(this.s.topology, updateOneOperation, callback);
return executeOperation(
this.s.topology,
new UpdateOneOperation(this, filter, update, options),
callback
);
};
/**
@@ -795,9 +790,11 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) {
options.ignoreUndefined = this.s.options.ignoreUndefined;
}
const replaceOneOperation = new ReplaceOneOperation(this, filter, doc, options);
return executeOperation(this.s.topology, replaceOneOperation, callback);
return executeOperation(
this.s.topology,
new ReplaceOneOperation(this, filter, doc, options),
callback
);
};
/**
@@ -823,14 +820,6 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) {
*/
Collection.prototype.updateMany = function(filter, update, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
const err = checkForAtomicOperators(update);
if (err) {
if (typeof callback === 'function') return callback(err);
return this.s.promiseLibrary.reject(err);
}
options = Object.assign({}, options);
// Add ignoreUndefined
@@ -839,9 +828,11 @@ Collection.prototype.updateMany = function(filter, update, options, callback) {
options.ignoreUndefined = this.s.options.ignoreUndefined;
}
const updateManyOperation = new UpdateManyOperation(this, filter, update, options);
return executeOperation(this.s.topology, updateManyOperation, callback);
return executeOperation(
this.s.topology,
new UpdateManyOperation(this, filter, update, options),
callback
);
};
/**
@@ -913,6 +904,7 @@ Collection.prototype.update = deprecate(function(selector, update, options, call
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {string|object} [options.hint] optional index hint for optimizing the filter query
* @param {Collection~deleteWriteOpCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
@@ -946,6 +938,7 @@ Collection.prototype.removeOne = Collection.prototype.deleteOne;
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {string|object} [options.hint] optional index hint for optimizing the filter query
* @param {Collection~deleteWriteOpCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
@@ -1206,6 +1199,7 @@ Collection.prototype.isCapped = function(options, callback) {
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Collection~resultCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
* @example
@@ -1232,14 +1226,14 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
const createIndexOperation = new CreateIndexOperation(
this.s.db,
const createIndexesOperation = new CreateIndexesOperation(
this,
this.collectionName,
fieldOrSpec,
options
);
return executeOperation(this.s.topology, createIndexOperation, callback);
return executeOperation(this.s.topology, createIndexesOperation, callback);
};
/**
@@ -1260,6 +1254,7 @@ Collection.prototype.createIndex = function(fieldOrSpec, options, callback) {
* @param {Collection~IndexDefinition[]} indexSpecs An array of index specifications to be created
* @param {Object} [options] Optional settings
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Collection~resultCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
* @example
@@ -1284,9 +1279,15 @@ Collection.prototype.createIndexes = function(indexSpecs, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options ? Object.assign({}, options) : {};
if (typeof options.maxTimeMS !== 'number') delete options.maxTimeMS;
const createIndexesOperation = new CreateIndexesOperation(this, indexSpecs, options);
const createIndexesOperation = new CreateIndexesOperation(
this,
this.collectionName,
indexSpecs,
options
);
return executeOperation(this.s.topology, createIndexesOperation, callback);
};
@@ -1353,19 +1354,20 @@ Collection.prototype.dropAllIndexes = deprecate(
* Reindex all indexes on the collection
* Warning: reIndex is a blocking operation (indexes are rebuilt in the foreground) and will be slow for large collections.
* @method
* @deprecated use db.command instead
* @param {Object} [options] Optional settings
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {Collection~resultCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
Collection.prototype.reIndex = function(options, callback) {
Collection.prototype.reIndex = deprecate(function(options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
const reIndexOperation = new ReIndexOperation(this, options);
return executeOperation(this.s.topology, reIndexOperation, callback);
};
}, 'collection.reIndex is deprecated. Use db.command instead.');
/**
* Get the list of all indexes information for the collection.
@@ -1665,13 +1667,11 @@ Collection.prototype.findOneAndDelete = function(filter, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
// Basic validation
if (filter == null || typeof filter !== 'object')
throw toError('filter parameter must be an object');
const findOneAndDeleteOperation = new FindOneAndDeleteOperation(this, filter, options);
return executeOperation(this.s.topology, findOneAndDeleteOperation, callback);
return executeOperation(
this.s.topology,
new FindOneAndDeleteOperation(this, filter, options),
callback
);
};
/**
@@ -1683,6 +1683,7 @@ Collection.prototype.findOneAndDelete = function(filter, options, callback) {
* @param {object} [options] Optional settings.
* @param {boolean} [options.bypassDocumentValidation=false] Allow driver to bypass schema validation in MongoDB 3.2 or higher.
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {string|object} [options.hint] An optional index to use for this operation
* @param {number} [options.maxTimeMS] The maximum amount of time to allow the query to run.
* @param {object} [options.projection] Limits the fields to return for all matching documents.
* @param {object} [options.sort] Determines which document the operation modifies if the query selects multiple documents.
@@ -1699,27 +1700,11 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options,
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
// Basic validation
if (filter == null || typeof filter !== 'object')
throw toError('filter parameter must be an object');
if (replacement == null || typeof replacement !== 'object')
throw toError('replacement parameter must be an object');
// Check that there are no atomic operators
const keys = Object.keys(replacement);
if (keys[0] && keys[0][0] === '$') {
throw toError('The replacement document must not contain atomic operators.');
}
const findOneAndReplaceOperation = new FindOneAndReplaceOperation(
this,
filter,
replacement,
options
return executeOperation(
this.s.topology,
new FindOneAndReplaceOperation(this, filter, replacement, options),
callback
);
return executeOperation(this.s.topology, findOneAndReplaceOperation, callback);
};
/**
@@ -1732,6 +1717,7 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options,
* @param {Array} [options.arrayFilters] optional list of array filters referenced in filtered positional operators
* @param {boolean} [options.bypassDocumentValidation=false] Allow driver to bypass schema validation in MongoDB 3.2 or higher.
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {string|object} [options.hint] An optional index to use for this operation
* @param {number} [options.maxTimeMS] The maximum amount of time to allow the query to run.
* @param {object} [options.projection] Limits the fields to return for all matching documents.
* @param {object} [options.sort] Determines which document the operation modifies if the query selects multiple documents.
@@ -1740,7 +1726,7 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options,
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {ClientSession} [options.session] An ptional session to use for this operation
* @param {Collection~findAndModifyCallback} [callback] The collection result callback
* @return {Promise<Collection~findAndModifyWriteOpResultObject>} returns Promise if no callback passed
*/
@@ -1748,21 +1734,11 @@ Collection.prototype.findOneAndUpdate = function(filter, update, options, callba
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
// Basic validation
if (filter == null || typeof filter !== 'object')
throw toError('filter parameter must be an object');
if (update == null || typeof update !== 'object')
throw toError('update parameter must be an object');
const err = checkForAtomicOperators(update);
if (err) {
if (typeof callback === 'function') return callback(err);
return this.s.promiseLibrary.reject(err);
}
const findOneAndUpdateOperation = new FindOneAndUpdateOperation(this, filter, update, options);
return executeOperation(this.s.topology, findOneAndUpdateOperation, callback);
return executeOperation(
this.s.topology,
new FindOneAndUpdateOperation(this, filter, update, options),
callback
);
};
/**
@@ -1864,7 +1840,7 @@ Collection.prototype.findAndRemove = deprecate(function(query, sort, options, ca
* @param {boolean} [options.promoteLongs=true] Promotes Long values to number if they fit inside the 53 bits resolution.
* @param {boolean} [options.promoteValues=true] Promotes BSON values to native types where possible, set to false to only receive wrapper types.
* @param {boolean} [options.promoteBuffers=false] Promotes Binary BSON values to native Node Buffers.
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
* @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
* @param {string} [options.comment] Add a comment to an aggregation command
* @param {string|object} [options.hint] Add an index selection hint to an aggregation command
* @param {ClientSession} [options.session] optional session to use for this operation
@@ -1978,7 +1954,7 @@ Collection.prototype.parallelCollectionScan = deprecate(function(options, callba
options = Object.assign({}, options);
// Ensure we have the right read preference inheritance
options.readPreference = resolveReadPreference(this, options);
options.readPreference = ReadPreference.resolve(this, options);
// Add a promiseLibrary
options.promiseLibrary = this.s.promiseLibrary;
@@ -2009,8 +1985,9 @@ Collection.prototype.parallelCollectionScan = deprecate(function(options, callba
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {Collection~resultCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
* @deprecated See {@link https://docs.mongodb.com/manual/geospatial-queries/|geospatial queries docs} for current geospatial support
*/
Collection.prototype.geoHaystackSearch = function(x, y, options, callback) {
Collection.prototype.geoHaystackSearch = deprecate(function(x, y, options, callback) {
const args = Array.prototype.slice.call(arguments, 2);
callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
options = args.length ? args.shift() || {} : {};
@@ -2018,7 +1995,7 @@ Collection.prototype.geoHaystackSearch = function(x, y, options, callback) {
const geoHaystackSearchOperation = new GeoHaystackSearchOperation(this, x, y, options);
return executeOperation(this.s.topology, geoHaystackSearchOperation, callback);
};
}, 'geoHaystackSearch is deprecated, and will be removed in a future version.');
/**
* Run a group command across a collection

View File

@@ -1,158 +1,55 @@
'use strict';
const MongoError = require('../error').MongoError;
/**
* Creates a new AuthProvider, which dictates how to authenticate for a given
* mechanism.
* @class
* Context used during authentication
*
* @property {Connection} connection The connection to authenticate
* @property {MongoCredentials} credentials The credentials to use for authentication
* @property {object} options The options passed to the `connect` method
* @property {object?} response The response of the initial handshake
* @property {Buffer?} nonce A random nonce generated for use in an authentication conversation
*/
class AuthContext {
constructor(connection, credentials, options) {
this.connection = connection;
this.credentials = credentials;
this.options = options;
}
}
class AuthProvider {
constructor(bson) {
this.bson = bson;
this.authStore = [];
}
/**
* Prepare the handshake document before the initial handshake.
*
* @param {object} handshakeDoc The document used for the initial handshake on a connection
* @param {AuthContext} authContext Context for authentication flow
* @param {function} callback
*/
prepare(handshakeDoc, context, callback) {
callback(undefined, handshakeDoc);
}
/**
* Authenticate
* @method
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {MongoCredentials} credentials Authentication credentials
*
* @param {AuthContext} context A shared context for authentication flow
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
auth(sendAuthCommand, connections, credentials, callback) {
// Total connections
let count = connections.length;
if (count === 0) {
callback(null, null);
return;
}
// Valid connections
let numberOfValidConnections = 0;
let errorObject = null;
const execute = connection => {
this._authenticateSingleConnection(sendAuthCommand, connection, credentials, (err, r) => {
// Adjust count
count = count - 1;
// If we have an error
if (err) {
errorObject = new MongoError(err);
} else if (r && (r.$err || r.errmsg)) {
errorObject = new MongoError(r);
} else {
numberOfValidConnections = numberOfValidConnections + 1;
}
// Still authenticating against other connections.
if (count !== 0) {
return;
}
// We have authenticated all connections
if (numberOfValidConnections > 0) {
// Store the auth details
this.addCredentials(credentials);
// Return correct authentication
callback(null, true);
} else {
if (errorObject == null) {
errorObject = new MongoError(`failed to authenticate using ${credentials.mechanism}`);
}
callback(errorObject, false);
}
});
};
const executeInNextTick = _connection => process.nextTick(() => execute(_connection));
// For each connection we need to authenticate
while (connections.length > 0) {
executeInNextTick(connections.shift());
}
}
/**
* Implementation of a single connection authenticating. Is meant to be overridden.
* Will error if called directly
* @ignore
*/
_authenticateSingleConnection(/*sendAuthCommand, connection, credentials, callback*/) {
throw new Error('_authenticateSingleConnection must be overridden');
}
/**
* Adds credentials to store only if it does not exist
* @param {MongoCredentials} credentials credentials to add to store
*/
addCredentials(credentials) {
const found = this.authStore.some(cred => cred.equals(credentials));
if (!found) {
this.authStore.push(credentials);
}
}
/**
* Re authenticate pool
* @method
* @param {SendAuthCommand} sendAuthCommand Writes an auth command directly to a specific connection
* @param {Connection[]} connections Connections to authenticate using this authenticator
* @param {authResultCallback} callback The callback to return the result from the authentication
*/
reauthenticate(sendAuthCommand, connections, callback) {
const authStore = this.authStore.slice(0);
let count = authStore.length;
if (count === 0) {
return callback(null, null);
}
for (let i = 0; i < authStore.length; i++) {
this.auth(sendAuthCommand, connections, authStore[i], function(err) {
count = count - 1;
if (count === 0) {
callback(err, null);
}
});
}
}
/**
* Remove credentials that have been previously stored in the auth provider
* @method
* @param {string} source Name of database we are removing authStore details about
* @return {object}
*/
logout(source) {
this.authStore = this.authStore.filter(credentials => credentials.source !== source);
auth(context, callback) {
callback(new TypeError('`auth` method must be overridden by subclass'));
}
}
/**
* A function that writes authentication commands to a specific connection
* @callback SendAuthCommand
* @param {Connection} connection The connection to write to
* @param {Command} command A command with a toBin method that can be written to a connection
* @param {AuthWriteCallback} callback Callback called when command response is received
*/
/**
* A callback for a specific auth command
* @callback AuthWriteCallback
* @param {Error} err If command failed, an error from the server
* @param {object} r The response from the server
*/
/**
* This is a result from an authentication strategy
* This is a result from an authentication provider
*
* @callback authResultCallback
* @param {error} error An error object. Set to null if no error present
* @param {boolean} result The result of the authentication process
*/
module.exports = { AuthProvider };
module.exports = { AuthContext, AuthProvider };

View File

@@ -4,9 +4,9 @@ const MongoCR = require('./mongocr');
const X509 = require('./x509');
const Plain = require('./plain');
const GSSAPI = require('./gssapi');
const SSPI = require('./sspi');
const ScramSHA1 = require('./scram').ScramSHA1;
const ScramSHA256 = require('./scram').ScramSHA256;
const MongoDBAWS = require('./mongodb_aws');
/**
* Returns the default authentication providers.
@@ -16,11 +16,11 @@ const ScramSHA256 = require('./scram').ScramSHA256;
*/
function defaultAuthProviders(bson) {
return {
'mongodb-aws': new MongoDBAWS(bson),
mongocr: new MongoCR(bson),
x509: new X509(bson),
plain: new Plain(bson),
gssapi: new GSSAPI(bson),
sspi: new SSPI(bson),
'scram-sha-1': new ScramSHA1(bson),
'scram-sha-256': new ScramSHA256(bson)
};

View File

@@ -1,50 +1,13 @@
'use strict';
const AuthProvider = require('./auth_provider').AuthProvider;
const retrieveKerberos = require('../utils').retrieveKerberos;
let kerberos;
/**
* Creates a new GSSAPI authentication mechanism
* @class
* @extends AuthProvider
*/
class GSSAPI extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
const source = credentials.source;
const username = credentials.username;
const password = credentials.password;
const mechanismProperties = credentials.mechanismProperties;
const gssapiServiceName =
mechanismProperties['gssapiservicename'] ||
mechanismProperties['gssapiServiceName'] ||
'mongodb';
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
GSSAPIInitialize(
this,
kerberos.processes.MongoAuthProcess,
source,
username,
password,
source,
gssapiServiceName,
sendAuthCommand,
connection,
mechanismProperties,
callback
);
}
/**
* Authenticate
* @override
* @method
*/
auth(sendAuthCommand, connections, credentials, callback) {
if (kerberos == null) {
try {
kerberos = retrieveKerberos();
@@ -53,189 +16,76 @@ class GSSAPI extends AuthProvider {
}
}
super.auth(sendAuthCommand, connections, credentials, callback);
// TODO: Destructure this
const username = credentials.username;
const password = credentials.password;
const mechanismProperties = credentials.mechanismProperties;
const gssapiServiceName =
mechanismProperties['gssapiservicename'] ||
mechanismProperties['gssapiServiceName'] ||
'mongodb';
const MongoAuthProcess = kerberos.processes.MongoAuthProcess;
const authProcess = new MongoAuthProcess(
connection.host,
connection.port,
gssapiServiceName,
mechanismProperties
);
authProcess.init(username, password, err => {
if (err) return callback(err, false);
authProcess.transition('', (err, payload) => {
if (err) return callback(err, false);
const command = {
saslStart: 1,
mechanism: 'GSSAPI',
payload,
autoAuthorize: 1
};
connection.command('$external.$cmd', command, (err, result) => {
if (err) return callback(err, false);
const doc = result.result;
authProcess.transition(doc.payload, (err, payload) => {
if (err) return callback(err, false);
const command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload
};
connection.command('$external.$cmd', command, (err, result) => {
if (err) return callback(err, false);
const doc = result.result;
authProcess.transition(doc.payload, (err, payload) => {
if (err) return callback(err, false);
const command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload
};
connection.command('$external.$cmd', command, (err, result) => {
if (err) return callback(err, false);
const response = result.result;
authProcess.transition(null, err => {
if (err) return callback(err, null);
callback(null, response);
});
});
});
});
});
});
});
});
}
}
//
// Initialize step
var GSSAPIInitialize = function(
self,
MongoAuthProcess,
db,
username,
password,
authdb,
gssapiServiceName,
sendAuthCommand,
connection,
options,
callback
) {
// Create authenticator
var mongo_auth_process = new MongoAuthProcess(
connection.host,
connection.port,
gssapiServiceName,
options
);
// Perform initialization
mongo_auth_process.init(username, password, function(err) {
if (err) return callback(err, false);
// Perform the first step
mongo_auth_process.transition('', function(err, payload) {
if (err) return callback(err, false);
// Call the next db step
MongoDBGSSAPIFirstStep(
self,
mongo_auth_process,
payload,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
);
});
});
};
//
// Perform first step against mongodb
var MongoDBGSSAPIFirstStep = function(
self,
mongo_auth_process,
payload,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build the sasl start command
var command = {
saslStart: 1,
mechanism: 'GSSAPI',
payload: payload,
autoAuthorize: 1
};
// Write the commmand on the connection
sendAuthCommand(connection, '$external.$cmd', command, (err, doc) => {
if (err) return callback(err, false);
// Execute mongodb transition
mongo_auth_process.transition(doc.payload, function(err, payload) {
if (err) return callback(err, false);
// MongoDB API Second Step
MongoDBGSSAPISecondStep(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
);
});
});
};
//
// Perform first step against mongodb
var MongoDBGSSAPISecondStep = function(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build Authentication command to send to MongoDB
var command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload: payload
};
// Execute the command
// Write the commmand on the connection
sendAuthCommand(connection, '$external.$cmd', command, (err, doc) => {
if (err) return callback(err, false);
// Call next transition for kerberos
mongo_auth_process.transition(doc.payload, function(err, payload) {
if (err) return callback(err, false);
// Call the last and third step
MongoDBGSSAPIThirdStep(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
);
});
});
};
var MongoDBGSSAPIThirdStep = function(
self,
mongo_auth_process,
payload,
doc,
db,
username,
password,
authdb,
sendAuthCommand,
connection,
callback
) {
// Build final command
var command = {
saslContinue: 1,
conversationId: doc.conversationId,
payload: payload
};
// Execute the command
sendAuthCommand(connection, '$external.$cmd', command, (err, r) => {
if (err) return callback(err, false);
mongo_auth_process.transition(null, function(err) {
if (err) return callback(err, null);
callback(null, r);
});
});
};
/**
* This is a result from a authentication strategy
*
* @callback authResultCallback
* @param {error} error An error object. Set to null if no error present
* @param {boolean} result The result of the authentication process
*/
module.exports = GSSAPI;

View File

@@ -47,7 +47,24 @@ class MongoCredentials {
this.password = options.password;
this.source = options.source || options.db;
this.mechanism = options.mechanism || 'default';
this.mechanismProperties = options.mechanismProperties;
this.mechanismProperties = options.mechanismProperties || {};
if (this.mechanism.match(/MONGODB-AWS/i)) {
if (this.username == null && process.env.AWS_ACCESS_KEY_ID) {
this.username = process.env.AWS_ACCESS_KEY_ID;
}
if (this.password == null && process.env.AWS_SECRET_ACCESS_KEY) {
this.password = process.env.AWS_SECRET_ACCESS_KEY;
}
if (this.mechanismProperties.AWS_SESSION_TOKEN == null && process.env.AWS_SESSION_TOKEN) {
this.mechanismProperties.AWS_SESSION_TOKEN = process.env.AWS_SESSION_TOKEN;
}
}
Object.freeze(this.mechanismProperties);
Object.freeze(this);
}
/**
@@ -69,12 +86,21 @@ class MongoCredentials {
* based on the server version and server supported sasl mechanisms.
*
* @param {Object} [ismaster] An ismaster response from the server
* @returns {MongoCredentials}
*/
resolveAuthMechanism(ismaster) {
// If the mechanism is not "default", then it does not need to be resolved
if (this.mechanism.toLowerCase() === 'default') {
this.mechanism = getDefaultAuthMechanism(ismaster);
if (this.mechanism.match(/DEFAULT/i)) {
return new MongoCredentials({
username: this.username,
password: this.password,
source: this.source,
mechanism: getDefaultAuthMechanism(ismaster),
mechanismProperties: this.mechanismProperties
});
}
return this;
}
}

View File

@@ -3,27 +3,21 @@
const crypto = require('crypto');
const AuthProvider = require('./auth_provider').AuthProvider;
/**
* Creates a new MongoCR authentication mechanism
*
* @extends AuthProvider
*/
class MongoCR extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const username = credentials.username;
const password = credentials.password;
const source = credentials.source;
sendAuthCommand(connection, `${source}.$cmd`, { getnonce: 1 }, (err, r) => {
connection.command(`${source}.$cmd`, { getnonce: 1 }, (err, result) => {
let nonce = null;
let key = null;
// Get nonce
if (err == null) {
const r = result.result;
nonce = r.nonce;
// Use node md5 generator
let md5 = crypto.createHash('md5');
@@ -43,7 +37,7 @@ class MongoCR extends AuthProvider {
key
};
sendAuthCommand(connection, `${source}.$cmd`, authenticateCommand, callback);
connection.command(`${source}.$cmd`, authenticateCommand, callback);
});
}
}

View File

@@ -1,5 +1,4 @@
'use strict';
const retrieveBSON = require('../connection/utils').retrieveBSON;
const AuthProvider = require('./auth_provider').AuthProvider;
@@ -7,19 +6,13 @@ const AuthProvider = require('./auth_provider').AuthProvider;
const BSON = retrieveBSON();
const Binary = BSON.Binary;
/**
* Creates a new Plain authentication mechanism
*
* @extends AuthProvider
*/
class Plain extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const username = credentials.username;
const password = credentials.password;
const payload = new Binary(`\x00${username}\x00${password}`);
const command = {
saslStart: 1,
@@ -28,7 +21,7 @@ class Plain extends AuthProvider {
autoAuthorize: 1
};
sendAuthCommand(connection, '$external.$cmd', command, callback);
connection.command('$external.$cmd', command, callback);
}
}

View File

@@ -1,5 +1,4 @@
'use strict';
const crypto = require('crypto');
const Buffer = require('safe-buffer').Buffer;
const retrieveBSON = require('../connection/utils').retrieveBSON;
@@ -16,32 +15,236 @@ try {
// don't do anything;
}
var parsePayload = function(payload) {
var dict = {};
var parts = payload.split(',');
for (var i = 0; i < parts.length; i++) {
var valueParts = parts[i].split('=');
class ScramSHA extends AuthProvider {
constructor(bson, cryptoMethod) {
super(bson);
this.cryptoMethod = cryptoMethod || 'sha1';
}
prepare(handshakeDoc, authContext, callback) {
const cryptoMethod = this.cryptoMethod;
if (cryptoMethod === 'sha256' && saslprep == null) {
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
}
crypto.randomBytes(24, (err, nonce) => {
if (err) {
return callback(err);
}
// store the nonce for later use
Object.assign(authContext, { nonce });
const credentials = authContext.credentials;
const request = Object.assign({}, handshakeDoc, {
speculativeAuthenticate: Object.assign(makeFirstMessage(cryptoMethod, credentials, nonce), {
db: credentials.source
})
});
callback(undefined, request);
});
}
auth(authContext, callback) {
const response = authContext.response;
if (response && response.speculativeAuthenticate) {
continueScramConversation(
this.cryptoMethod,
response.speculativeAuthenticate,
authContext,
callback
);
return;
}
executeScram(this.cryptoMethod, authContext, callback);
}
}
function cleanUsername(username) {
return username.replace('=', '=3D').replace(',', '=2C');
}
function clientFirstMessageBare(username, nonce) {
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
return Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce.toString('base64'), 'utf8')
]);
}
function makeFirstMessage(cryptoMethod, credentials, nonce) {
const username = cleanUsername(credentials.username);
const mechanism = cryptoMethod === 'sha1' ? 'SCRAM-SHA-1' : 'SCRAM-SHA-256';
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
return {
saslStart: 1,
mechanism,
payload: new Binary(
Buffer.concat([Buffer.from('n,,', 'utf8'), clientFirstMessageBare(username, nonce)])
),
autoAuthorize: 1,
options: { skipEmptyExchange: true }
};
}
function executeScram(cryptoMethod, authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const nonce = authContext.nonce;
const db = credentials.source;
const saslStartCmd = makeFirstMessage(cryptoMethod, credentials, nonce);
connection.command(`${db}.$cmd`, saslStartCmd, (_err, result) => {
const err = resolveError(_err, result);
if (err) {
return callback(err);
}
continueScramConversation(cryptoMethod, result.result, authContext, callback);
});
}
function continueScramConversation(cryptoMethod, response, authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const nonce = authContext.nonce;
const db = credentials.source;
const username = cleanUsername(credentials.username);
const password = credentials.password;
let processedPassword;
if (cryptoMethod === 'sha256') {
processedPassword = saslprep ? saslprep(password) : password;
} else {
try {
processedPassword = passwordDigest(username, password);
} catch (e) {
return callback(e);
}
}
const payload = Buffer.isBuffer(response.payload)
? new Binary(response.payload)
: response.payload;
const dict = parsePayload(payload.value());
const iterations = parseInt(dict.i, 10);
if (iterations && iterations < 4096) {
callback(new MongoError(`Server returned an invalid iteration count ${iterations}`), false);
return;
}
const salt = dict.s;
const rnonce = dict.r;
if (rnonce.startsWith('nonce')) {
callback(new MongoError(`Server returned an invalid nonce: ${rnonce}`), false);
return;
}
// Set up start of proof
const withoutProof = `c=biws,r=${rnonce}`;
const saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
const storedKey = H(cryptoMethod, clientKey);
const authMessage = [
clientFirstMessageBare(username, nonce),
payload.value().toString('base64'),
withoutProof
].join(',');
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
const clientProof = `p=${xor(clientKey, clientSignature)}`;
const clientFinal = [withoutProof, clientProof].join(',');
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
const saslContinueCmd = {
saslContinue: 1,
conversationId: response.conversationId,
payload: new Binary(Buffer.from(clientFinal))
};
connection.command(`${db}.$cmd`, saslContinueCmd, (_err, result) => {
const err = resolveError(_err, result);
if (err) {
return callback(err);
}
const r = result.result;
const parsedResponse = parsePayload(r.payload.value());
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
callback(new MongoError('Server returned an invalid signature'));
return;
}
if (!r || r.done !== false) {
return callback(err, r);
}
const retrySaslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: Buffer.alloc(0)
};
connection.command(`${db}.$cmd`, retrySaslContinueCmd, callback);
});
}
function parsePayload(payload) {
const dict = {};
const parts = payload.split(',');
for (let i = 0; i < parts.length; i++) {
const valueParts = parts[i].split('=');
dict[valueParts[0]] = valueParts[1];
}
return dict;
};
}
var passwordDigest = function(username, password) {
if (typeof username !== 'string') throw new MongoError('username must be a string');
if (typeof password !== 'string') throw new MongoError('password must be a string');
if (password.length === 0) throw new MongoError('password cannot be empty');
// Use node md5 generator
var md5 = crypto.createHash('md5');
// Generate keys used for authentication
md5.update(username + ':mongo:' + password, 'utf8');
function passwordDigest(username, password) {
if (typeof username !== 'string') {
throw new MongoError('username must be a string');
}
if (typeof password !== 'string') {
throw new MongoError('password must be a string');
}
if (password.length === 0) {
throw new MongoError('password cannot be empty');
}
const md5 = crypto.createHash('md5');
md5.update(`${username}:mongo:${password}`, 'utf8');
return md5.digest('hex');
};
}
// XOR two buffers
function xor(a, b) {
if (!Buffer.isBuffer(a)) a = Buffer.from(a);
if (!Buffer.isBuffer(b)) b = Buffer.from(b);
if (!Buffer.isBuffer(a)) {
a = Buffer.from(a);
}
if (!Buffer.isBuffer(b)) {
b = Buffer.from(b);
}
const length = Math.max(a.length, b.length);
const res = [];
@@ -66,12 +269,12 @@ function HMAC(method, key, text) {
.digest();
}
var _hiCache = {};
var _hiCacheCount = 0;
var _hiCachePurge = function() {
let _hiCache = {};
let _hiCacheCount = 0;
function _hiCachePurge() {
_hiCache = {};
_hiCacheCount = 0;
};
}
const hiLengthMap = {
sha256: 32,
@@ -121,205 +324,19 @@ function compareDigest(lhs, rhs) {
return result === 0;
}
/**
* Creates a new ScramSHA authentication mechanism
* @class
* @extends AuthProvider
*/
class ScramSHA extends AuthProvider {
constructor(bson, cryptoMethod) {
super(bson);
this.cryptoMethod = cryptoMethod || 'sha1';
}
function resolveError(err, result) {
if (err) return err;
static _getError(err, r) {
if (err) {
return err;
}
if (r.$err || r.errmsg) {
return new MongoError(r);
}
}
/**
* @ignore
*/
_executeScram(sendAuthCommand, connection, credentials, nonce, callback) {
let username = credentials.username;
const password = credentials.password;
const db = credentials.source;
const cryptoMethod = this.cryptoMethod;
let mechanism = 'SCRAM-SHA-1';
let processedPassword;
if (cryptoMethod === 'sha256') {
mechanism = 'SCRAM-SHA-256';
processedPassword = saslprep ? saslprep(password) : password;
} else {
try {
processedPassword = passwordDigest(username, password);
} catch (e) {
return callback(e);
}
}
// Clean up the user
username = username.replace('=', '=3D').replace(',', '=2C');
// NOTE: This is done b/c Javascript uses UTF-16, but the server is hashing in UTF-8.
// Since the username is not sasl-prep-d, we need to do this here.
const firstBare = Buffer.concat([
Buffer.from('n=', 'utf8'),
Buffer.from(username, 'utf8'),
Buffer.from(',r=', 'utf8'),
Buffer.from(nonce, 'utf8')
]);
// Build command structure
const saslStartCmd = {
saslStart: 1,
mechanism,
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
autoAuthorize: 1
};
// Write the commmand on the connection
sendAuthCommand(connection, `${db}.$cmd`, saslStartCmd, (err, r) => {
let tmpError = ScramSHA._getError(err, r);
if (tmpError) {
return callback(tmpError, null);
}
const payload = Buffer.isBuffer(r.payload) ? new Binary(r.payload) : r.payload;
const dict = parsePayload(payload.value());
const iterations = parseInt(dict.i, 10);
if (iterations && iterations < 4096) {
callback(new MongoError(`Server returned an invalid iteration count ${iterations}`), false);
return;
}
const salt = dict.s;
const rnonce = dict.r;
if (rnonce.startsWith('nonce')) {
callback(new MongoError(`Server returned an invalid nonce: ${rnonce}`), false);
return;
}
// Set up start of proof
const withoutProof = `c=biws,r=${rnonce}`;
const saltedPassword = HI(
processedPassword,
Buffer.from(salt, 'base64'),
iterations,
cryptoMethod
);
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = HMAC(cryptoMethod, saltedPassword, 'Server Key');
const storedKey = H(cryptoMethod, clientKey);
const authMessage = [firstBare, payload.value().toString('base64'), withoutProof].join(',');
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
const clientProof = `p=${xor(clientKey, clientSignature)}`;
const clientFinal = [withoutProof, clientProof].join(',');
const serverSignature = HMAC(cryptoMethod, serverKey, authMessage);
const saslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: new Binary(Buffer.from(clientFinal))
};
sendAuthCommand(connection, `${db}.$cmd`, saslContinueCmd, (err, r) => {
if (err || (r && typeof r.ok === 'number' && r.ok === 0)) {
callback(err, r);
return;
}
const parsedResponse = parsePayload(r.payload.value());
if (!compareDigest(Buffer.from(parsedResponse.v, 'base64'), serverSignature)) {
callback(new MongoError('Server returned an invalid signature'));
return;
}
if (!r || r.done !== false) {
return callback(err, r);
}
const retrySaslContinueCmd = {
saslContinue: 1,
conversationId: r.conversationId,
payload: Buffer.alloc(0)
};
sendAuthCommand(connection, `${db}.$cmd`, retrySaslContinueCmd, callback);
});
});
}
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
// Create a random nonce
crypto.randomBytes(24, (err, buff) => {
if (err) {
return callback(err, null);
}
return this._executeScram(
sendAuthCommand,
connection,
credentials,
buff.toString('base64'),
callback
);
});
}
/**
* Authenticate
* @override
* @method
*/
auth(sendAuthCommand, connections, credentials, callback) {
this._checkSaslprep();
super.auth(sendAuthCommand, connections, credentials, callback);
}
_checkSaslprep() {
const cryptoMethod = this.cryptoMethod;
if (cryptoMethod === 'sha256') {
if (!saslprep) {
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
}
}
}
const r = result.result;
if (r.$err || r.errmsg) return new MongoError(r);
}
/**
* Creates a new ScramSHA1 authentication mechanism
* @class
* @extends ScramSHA
*/
class ScramSHA1 extends ScramSHA {
constructor(bson) {
super(bson, 'sha1');
}
}
/**
* Creates a new ScramSHA256 authentication mechanism
* @class
* @extends ScramSHA
*/
class ScramSHA256 extends ScramSHA {
constructor(bson) {
super(bson, 'sha256');

View File

@@ -1,26 +1,35 @@
'use strict';
const AuthProvider = require('./auth_provider').AuthProvider;
/**
* Creates a new X509 authentication mechanism
* @class
* @extends AuthProvider
*/
class X509 extends AuthProvider {
/**
* Implementation of authentication for a single connection
* @override
*/
_authenticateSingleConnection(sendAuthCommand, connection, credentials, callback) {
const username = credentials.username;
const command = { authenticate: 1, mechanism: 'MONGODB-X509' };
if (username) {
command.user = username;
prepare(handshakeDoc, authContext, callback) {
const credentials = authContext.credentials;
Object.assign(handshakeDoc, {
speculativeAuthenticate: x509AuthenticateCommand(credentials)
});
callback(undefined, handshakeDoc);
}
auth(authContext, callback) {
const connection = authContext.connection;
const credentials = authContext.credentials;
const response = authContext.response;
if (response.speculativeAuthenticate) {
return callback();
}
sendAuthCommand(connection, '$external.$cmd', command, callback);
connection.command('$external.$cmd', x509AuthenticateCommand(credentials), callback);
}
}
function x509AuthenticateCommand(credentials) {
const command = { authenticate: 1, mechanism: 'MONGODB-X509' };
if (credentials.username) {
Object.apply(command, { user: credentials.username });
}
return command;
}
module.exports = X509;

View File

@@ -2,10 +2,11 @@
const net = require('net');
const tls = require('tls');
const Connection = require('./connection');
const Query = require('./commands').Query;
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const defaultAuthProviders = require('../auth/defaultAuthProviders').defaultAuthProviders;
const AuthContext = require('../auth/auth_provider').AuthContext;
const WIRE_CONSTANTS = require('../wireprotocol/constants');
const makeClientMetadata = require('../utils').makeClientMetadata;
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
@@ -37,30 +38,7 @@ function connect(options, cancellationToken, callback) {
}
function isModernConnectionType(conn) {
return typeof conn.command === 'function';
}
function getSaslSupportedMechs(options) {
if (!(options && options.credentials)) {
return {};
}
const credentials = options.credentials;
// TODO: revisit whether or not items like `options.user` and `options.dbName` should be checked here
const authMechanism = credentials.mechanism;
const authSource = credentials.source || options.dbName || 'admin';
const user = credentials.username || options.user;
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
return {};
}
if (!user) {
return {};
}
return { saslSupportedMechs: `${authSource}.${user}` };
return !(conn instanceof Connection);
}
function checkSupportedServer(ismaster, options) {
@@ -97,77 +75,115 @@ function performInitialHandshake(conn, options, _callback) {
_callback(err, ret);
};
let compressors = [];
if (options.compression && options.compression.compressors) {
compressors = options.compression.compressors;
const credentials = options.credentials;
if (credentials) {
if (!credentials.mechanism.match(/DEFAULT/i) && !AUTH_PROVIDERS[credentials.mechanism]) {
callback(new MongoError(`authMechanism '${credentials.mechanism}' not supported`));
return;
}
}
const handshakeDoc = Object.assign(
{
ismaster: true,
client: options.metadata || makeClientMetadata(options),
compression: compressors
},
getSaslSupportedMechs(options)
);
const handshakeOptions = Object.assign({}, options);
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
if (options.connectTimeoutMS || options.connectionTimeout) {
handshakeOptions.socketTimeout = options.connectTimeoutMS || options.connectionTimeout;
}
const start = new Date().getTime();
runCommand(conn, 'admin.$cmd', handshakeDoc, handshakeOptions, (err, ismaster) => {
const authContext = new AuthContext(conn, credentials, options);
prepareHandshakeDocument(authContext, (err, handshakeDoc) => {
if (err) {
callback(err);
return;
return callback(err);
}
if (ismaster.ok === 0) {
callback(new MongoError(ismaster));
return;
const handshakeOptions = Object.assign({}, options);
if (options.connectTimeoutMS || options.connectionTimeout) {
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
handshakeOptions.socketTimeout = options.connectTimeoutMS || options.connectionTimeout;
}
const supportedServerErr = checkSupportedServer(ismaster, options);
if (supportedServerErr) {
callback(supportedServerErr);
return;
}
const start = new Date().getTime();
conn.command('admin.$cmd', handshakeDoc, handshakeOptions, (err, result) => {
if (err) {
callback(err);
return;
}
if (!isModernConnectionType(conn)) {
// resolve compression
if (ismaster.compression) {
const agreedCompressors = compressors.filter(
compressor => ismaster.compression.indexOf(compressor) !== -1
);
const response = result.result;
if (response.ok === 0) {
callback(new MongoError(response));
return;
}
if (agreedCompressors.length) {
conn.agreedCompressor = agreedCompressors[0];
}
const supportedServerErr = checkSupportedServer(response, options);
if (supportedServerErr) {
callback(supportedServerErr);
return;
}
if (options.compression && options.compression.zlibCompressionLevel) {
conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
if (!isModernConnectionType(conn)) {
// resolve compression
if (response.compression) {
const agreedCompressors = handshakeDoc.compression.filter(
compressor => response.compression.indexOf(compressor) !== -1
);
if (agreedCompressors.length) {
conn.agreedCompressor = agreedCompressors[0];
}
if (options.compression && options.compression.zlibCompressionLevel) {
conn.zlibCompressionLevel = options.compression.zlibCompressionLevel;
}
}
}
}
// NOTE: This is metadata attached to the connection while porting away from
// handshake being done in the `Server` class. Likely, it should be
// relocated, or at very least restructured.
conn.ismaster = ismaster;
conn.lastIsMasterMS = new Date().getTime() - start;
// NOTE: This is metadata attached to the connection while porting away from
// handshake being done in the `Server` class. Likely, it should be
// relocated, or at very least restructured.
conn.ismaster = response;
conn.lastIsMasterMS = new Date().getTime() - start;
const credentials = options.credentials;
if (!ismaster.arbiterOnly && credentials) {
credentials.resolveAuthMechanism(ismaster);
authenticate(conn, credentials, callback);
if (!response.arbiterOnly && credentials) {
// store the response on auth context
Object.assign(authContext, { response });
const resolvedCredentials = credentials.resolveAuthMechanism(response);
const authProvider = AUTH_PROVIDERS[resolvedCredentials.mechanism];
authProvider.auth(authContext, err => {
if (err) return callback(err);
callback(undefined, conn);
});
return;
}
callback(undefined, conn);
});
});
}
function prepareHandshakeDocument(authContext, callback) {
const options = authContext.options;
const compressors =
options.compression && options.compression.compressors ? options.compression.compressors : [];
const handshakeDoc = {
ismaster: true,
client: options.metadata || makeClientMetadata(options),
compression: compressors
};
const credentials = authContext.credentials;
if (credentials) {
if (credentials.mechanism.match(/DEFAULT/i) && credentials.username) {
Object.assign(handshakeDoc, {
saslSupportedMechs: `${credentials.source}.${credentials.username}`
});
AUTH_PROVIDERS['scram-sha-256'].prepare(handshakeDoc, authContext, callback);
return;
}
callback(undefined, conn);
});
const authProvider = AUTH_PROVIDERS[credentials.mechanism];
authProvider.prepare(handshakeDoc, authContext, callback);
return;
}
callback(undefined, handshakeDoc);
}
const LEGAL_SSL_SOCKET_OPTIONS = [
@@ -239,7 +255,7 @@ function makeConnection(family, options, cancellationToken, _callback) {
const useSsl = typeof options.ssl === 'boolean' ? options.ssl : false;
const keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
let keepAliveInitialDelay =
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 120000;
const noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
const connectionTimeout =
typeof options.connectionTimeout === 'number'
@@ -318,92 +334,12 @@ function makeConnection(family, options, cancellationToken, _callback) {
socket.once(connectEvent, connectHandler);
}
const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
function runCommand(conn, ns, command, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
// are we using the new connection type? if so, no need to simulate a rpc `command` method
if (isModernConnectionType(conn)) {
conn.command(ns, command, options, (err, result) => {
if (err) {
callback(err);
return;
}
// NODE-2382: raw wire protocol messages, or command results should not be used anymore
callback(undefined, result.result);
});
return;
}
const socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
const bson = conn.options.bson;
const query = new Query(bson, ns, command, {
numberToSkip: 0,
numberToReturn: 1
});
const noop = () => {};
function _callback(err, result) {
callback(err, result);
callback = noop;
}
function errorHandler(err) {
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
if (err == null) {
err = new MongoError(`runCommand failed for connection to '${conn.address}'`);
}
// ignore all future errors
conn.on('error', noop);
_callback(err);
}
function messageHandler(msg) {
if (msg.responseTo !== query.requestId) {
return;
}
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
msg.parse({ promoteValues: true });
_callback(undefined, msg.documents[0]);
}
conn.setSocketTimeout(socketTimeout);
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
conn.on('message', messageHandler);
conn.write(query.toBin());
}
function authenticate(conn, credentials, callback) {
const mechanism = credentials.mechanism;
if (!AUTH_PROVIDERS[mechanism]) {
callback(new MongoError(`authMechanism '${mechanism}' not supported`));
return;
}
const provider = AUTH_PROVIDERS[mechanism];
provider.auth(runCommand, [conn], credentials, err => {
if (err) return callback(err);
callback(undefined, conn);
});
}
function connectionFailureError(type, err) {
switch (type) {
case 'error':
return new MongoNetworkError(err);
case 'timeout':
return new MongoNetworkError(`connection timed out`);
return new MongoNetworkTimeoutError(`connection timed out`);
case 'close':
return new MongoNetworkError(`connection closed`);
case 'cancel':

View File

@@ -8,12 +8,15 @@ const decompress = require('../wireprotocol/compression').decompress;
const Response = require('./commands').Response;
const BinMsg = require('./msg').BinMsg;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const MongoError = require('../error').MongoError;
const Logger = require('./logger');
const OP_COMPRESSED = require('../wireprotocol/shared').opcodes.OP_COMPRESSED;
const OP_MSG = require('../wireprotocol/shared').opcodes.OP_MSG;
const MESSAGE_HEADER_SIZE = require('../wireprotocol/shared').MESSAGE_HEADER_SIZE;
const Buffer = require('safe-buffer').Buffer;
const Query = require('./commands').Query;
const CommandResult = require('./command_result');
let _id = 0;
@@ -64,7 +67,7 @@ class Connection extends EventEmitter {
* @param {string} [options.host='localhost'] The host the socket is connected to
* @param {number} [options.port=27017] The port used for the socket connection
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] TCP Socket timeout setting
* @param {boolean} [options.promoteLongs] Convert Long values from the db into Numbers if they fit into 53 bits
@@ -94,7 +97,7 @@ class Connection extends EventEmitter {
// These values are inspected directly in tests, but maybe not necessary to keep around
this.keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
this.keepAliveInitialDelay =
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 120000;
this.connectionTimeout =
typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
if (this.keepAliveInitialDelay > this.socketTimeout) {
@@ -305,8 +308,71 @@ class Connection extends EventEmitter {
if (this.destroyed) return false;
return !this.socket.destroyed && this.socket.writable;
}
/**
* @ignore
*/
command(ns, command, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
const conn = this;
const socketTimeout =
typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
const bson = conn.options.bson;
const query = new Query(bson, ns, command, {
numberToSkip: 0,
numberToReturn: 1
});
const noop = () => {};
function _callback(err, result) {
callback(err, result);
callback = noop;
}
function errorHandler(err) {
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
if (err == null) {
err = new MongoError(`runCommand failed for connection to '${conn.address}'`);
}
// ignore all future errors
conn.on('error', noop);
_callback(err);
}
function messageHandler(msg) {
if (msg.responseTo !== query.requestId) {
return;
}
conn.resetSocketTimeout();
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.removeListener(eventName, errorHandler));
conn.removeListener('message', messageHandler);
msg.parse({ promoteValues: true });
const response = msg.documents[0];
if (response.ok === 0 || response.$err || response.errmsg || response.code) {
_callback(new MongoError(response));
return;
}
_callback(undefined, new CommandResult(response, this, msg));
}
conn.setSocketTimeout(socketTimeout);
CONNECTION_ERROR_EVENTS.forEach(eventName => conn.once(eventName, errorHandler));
conn.on('message', messageHandler);
conn.write(query.toBin());
}
}
const CONNECTION_ERROR_EVENTS = ['error', 'close', 'timeout', 'parseError'];
function deleteConnection(id) {
// console.log("=== deleted connection " + id + " :: " + (connections[id] ? connections[id].port : ''))
delete connections[id];
@@ -352,7 +418,9 @@ function timeoutHandler(conn) {
conn.timedOut = true;
conn.emit(
'timeout',
new MongoNetworkError(`connection ${conn.id} to ${conn.address} timed out`),
new MongoNetworkTimeoutError(`connection ${conn.id} to ${conn.address} timed out`, {
beforeHandshake: conn.ismaster == null
}),
conn
);
};

View File

@@ -60,7 +60,7 @@ var _id = 0;
* @param {number} [options.reconnectTries=30] Server attempt to reconnect #times
* @param {number} [options.reconnectInterval=1000] Server will wait # milliseconds between retries
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] TCP Socket timeout setting
@@ -113,7 +113,7 @@ var Pool = function(topology, options) {
connectionTimeout: 30000,
socketTimeout: 360000,
keepAlive: true,
keepAliveInitialDelay: 300000,
keepAliveInitialDelay: 120000,
noDelay: true,
// SSL Settings
ssl: false,

View File

@@ -464,50 +464,41 @@ class CoreCursor extends Readable {
}
const result = r.message;
if (result.queryFailure) {
return done(new MongoError(result.documents[0]), null);
}
// Check if we have a command cursor
if (
Array.isArray(result.documents) &&
result.documents.length === 1 &&
(!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) &&
(typeof result.documents[0].cursor !== 'string' ||
result.documents[0]['$err'] ||
result.documents[0]['errmsg'] ||
Array.isArray(result.documents[0].result))
) {
// We have an error document, return the error
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
return done(new MongoError(result.documents[0]), null);
if (Array.isArray(result.documents) && result.documents.length === 1) {
const document = result.documents[0];
if (result.queryFailure) {
return done(new MongoError(document), null);
}
// We have a cursor document
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
const id = result.documents[0].cursor.id;
// If we have a namespace change set the new namespace for getmores
if (result.documents[0].cursor.ns) {
cursor.ns = result.documents[0].cursor.ns;
}
// Promote id to long if needed
cursor.cursorState.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
cursor.cursorState.lastCursorId = cursor.cursorState.cursorId;
cursor.cursorState.operationTime = result.documents[0].operationTime;
// If we have a firstBatch set it
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
cursor.cursorState.documents = result.documents[0].cursor.firstBatch; //.reverse();
// Check if we have a command cursor
if (!cursor.cmd.find || (cursor.cmd.find && cursor.cmd.virtual === false)) {
// We have an error document, return the error
if (document.$err || document.errmsg) {
return done(new MongoError(document), null);
}
// Return after processing command cursor
return done(null, result);
}
// We have a cursor document
if (document.cursor != null && typeof document.cursor !== 'string') {
const id = document.cursor.id;
// If we have a namespace change set the new namespace for getmores
if (document.cursor.ns) {
cursor.ns = document.cursor.ns;
}
// Promote id to long if needed
cursor.cursorState.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
cursor.cursorState.lastCursorId = cursor.cursorState.cursorId;
cursor.cursorState.operationTime = document.operationTime;
if (Array.isArray(result.documents[0].result)) {
cursor.cursorState.documents = result.documents[0].result;
cursor.cursorState.cursorId = Long.ZERO;
return done(null, result);
// If we have a firstBatch set it
if (Array.isArray(document.cursor.firstBatch)) {
cursor.cursorState.documents = document.cursor.firstBatch; //.reverse();
}
// Return after processing command cursor
return done(null, result);
}
}
}

View File

@@ -1,5 +1,7 @@
'use strict';
const kErrorLabels = Symbol('errorLabels');
/**
* Creates a new MongoError
*
@@ -18,8 +20,12 @@ class MongoError extends Error {
super(message);
} else {
super(message.message || message.errmsg || message.$err || 'n/a');
if (message.errorLabels) {
this[kErrorLabels] = new Set(message.errorLabels);
}
for (var name in message) {
if (name === 'errmsg') {
if (name === 'errorLabels' || name === 'errmsg') {
continue;
}
@@ -57,8 +63,29 @@ class MongoError extends Error {
* @returns {boolean} returns true if the error has the provided error label
*/
hasErrorLabel(label) {
return this.errorLabels && this.errorLabels.indexOf(label) !== -1;
if (this[kErrorLabels] == null) {
return false;
}
return this[kErrorLabels].has(label);
}
addErrorLabel(label) {
if (this[kErrorLabels] == null) {
this[kErrorLabels] = new Set();
}
this[kErrorLabels].add(label);
}
get errorLabels() {
return this[kErrorLabels] ? Array.from(this[kErrorLabels]) : [];
}
}
const kBeforeHandshake = Symbol('beforeHandshake');
function isNetworkErrorBeforeHandshake(err) {
return err[kBeforeHandshake] === true;
}
/**
@@ -71,9 +98,28 @@ class MongoError extends Error {
* @extends MongoError
*/
class MongoNetworkError extends MongoError {
constructor(message) {
constructor(message, options) {
super(message);
this.name = 'MongoNetworkError';
if (options && options.beforeHandshake === true) {
this[kBeforeHandshake] = true;
}
}
}
/**
* An error indicating a network timeout occurred
*
* @param {Error|string|object} message The error message
* @property {string} message The error message
* @property {object} [options.beforeHandshake] Indicates the timeout happened before a connection handshake completed
* @extends MongoError
*/
class MongoNetworkTimeoutError extends MongoNetworkError {
constructor(message, options) {
super(message, options);
this.name = 'MongoNetworkTimeoutError';
}
}
@@ -158,6 +204,10 @@ class MongoWriteConcernError extends MongoError {
super(message);
this.name = 'MongoWriteConcernError';
if (result && Array.isArray(result.errorLabels)) {
this[kErrorLabels] = new Set(result.errorLabels);
}
if (result != null) {
this.result = makeWriteConcernResultObject(result);
}
@@ -179,6 +229,32 @@ const RETRYABLE_ERROR_CODES = new Set([
13436 // NotMasterOrSecondary
]);
const RETRYABLE_WRITE_ERROR_CODES = new Set([
11600, // InterruptedAtShutdown
11602, // InterruptedDueToReplStateChange
10107, // NotMaster
13435, // NotMasterNoSlaveOk
13436, // NotMasterOrSecondary
189, // PrimarySteppedDown
91, // ShutdownInProgress
7, // HostNotFound
6, // HostUnreachable
89, // NetworkTimeout
9001, // SocketException
262 // ExceededTimeLimit
]);
function isRetryableWriteError(error) {
if (error instanceof MongoWriteConcernError) {
return (
RETRYABLE_WRITE_ERROR_CODES.has(error.code) ||
RETRYABLE_WRITE_ERROR_CODES.has(error.result.code)
);
}
return RETRYABLE_WRITE_ERROR_CODES.has(error.code);
}
/**
* Determines whether an error is something the driver should attempt to retry
*
@@ -259,13 +335,10 @@ function isSDAMUnrecoverableError(error) {
return false;
}
function isNetworkTimeoutError(err) {
return err instanceof MongoNetworkError && err.message.match(/timed out/);
}
module.exports = {
MongoError,
MongoNetworkError,
MongoNetworkTimeoutError,
MongoParseError,
MongoTimeoutError,
MongoServerSelectionError,
@@ -273,5 +346,6 @@ module.exports = {
isRetryableError,
isSDAMUnrecoverableError,
isNodeShuttingDownError,
isNetworkTimeoutError
isRetryableWriteError,
isNetworkErrorBeforeHandshake
};

View File

@@ -28,6 +28,13 @@ const ServerType = {
Unknown: 'Unknown'
};
// helper to get a server's type that works for both legacy and unified topologies
function serverType(server) {
let description = server.s.description || server.s.serverDescription;
if (description.topologyType === TopologyType.Single) return description.servers[0].type;
return description.type;
}
const TOPOLOGY_DEFAULTS = {
useUnifiedTopology: true,
localThresholdMS: 15,
@@ -54,6 +61,7 @@ module.exports = {
TOPOLOGY_DEFAULTS,
TopologyType,
ServerType,
serverType,
drainTimerQueue,
clearAndRemoveTimerFrom
};

View File

@@ -6,7 +6,8 @@ const connect = require('../connection/connect');
const Connection = require('../../cmap/connection').Connection;
const common = require('./common');
const makeStateMachine = require('../utils').makeStateMachine;
const MongoError = require('../error').MongoError;
const MongoNetworkError = require('../error').MongoNetworkError;
const BSON = require('../connection/utils').retrieveBSON();
const makeInterruptableAsyncInterval = require('../../utils').makeInterruptableAsyncInterval;
const calculateDurationInMs = require('../../utils').calculateDurationInMs;
const now = require('../../utils').now;
@@ -20,13 +21,15 @@ const kServer = Symbol('server');
const kMonitorId = Symbol('monitorId');
const kConnection = Symbol('connection');
const kCancellationToken = Symbol('cancellationToken');
const kRTTPinger = Symbol('rttPinger');
const kRoundTripTime = Symbol('roundTripTime');
const STATE_CLOSED = common.STATE_CLOSED;
const STATE_CLOSING = common.STATE_CLOSING;
const STATE_IDLE = 'idle';
const STATE_MONITORING = 'monitoring';
const stateTransition = makeStateMachine({
[STATE_CLOSING]: [STATE_CLOSING, STATE_CLOSED],
[STATE_CLOSING]: [STATE_CLOSING, STATE_IDLE, STATE_CLOSED],
[STATE_CLOSED]: [STATE_CLOSED, STATE_MONITORING],
[STATE_IDLE]: [STATE_IDLE, STATE_MONITORING, STATE_CLOSING],
[STATE_MONITORING]: [STATE_MONITORING, STATE_IDLE, STATE_CLOSING]
@@ -66,28 +69,29 @@ class Monitor extends EventEmitter {
});
// TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration
const addressParts = server.description.address.split(':');
this.connectOptions = Object.freeze(
Object.assign(
{
id: '<monitor>',
host: addressParts[0],
port: parseInt(addressParts[1], 10),
bson: server.s.bson,
connectionType: Connection
},
server.s.options,
this.options,
const connectOptions = Object.assign(
{
id: '<monitor>',
host: server.description.host,
port: server.description.port,
bson: server.s.bson,
connectionType: Connection
},
server.s.options,
this.options,
// force BSON serialization options
{
raw: false,
promoteLongs: true,
promoteValues: true,
promoteBuffers: true
}
)
// force BSON serialization options
{
raw: false,
promoteLongs: true,
promoteValues: true,
promoteBuffers: true
}
);
// ensure no authentication is used for monitoring
delete connectOptions.credentials;
this.connectOptions = Object.freeze(connectOptions);
}
connect() {
@@ -113,88 +117,165 @@ class Monitor extends EventEmitter {
this[kMonitorId].wake();
}
reset() {
if (isInCloseState(this)) {
return;
}
stateTransition(this, STATE_CLOSING);
resetMonitorState(this);
// restart monitor
stateTransition(this, STATE_IDLE);
// restart monitoring
const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;
const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;
this[kMonitorId] = makeInterruptableAsyncInterval(monitorServer(this), {
interval: heartbeatFrequencyMS,
minInterval: minHeartbeatFrequencyMS
});
}
close() {
if (isInCloseState(this)) {
return;
}
stateTransition(this, STATE_CLOSING);
this[kCancellationToken].emit('cancel');
if (this[kMonitorId]) {
this[kMonitorId].stop();
this[kMonitorId] = null;
}
if (this[kConnection]) {
this[kConnection].destroy({ force: true });
}
resetMonitorState(this);
// close monitor
this.emit('close');
stateTransition(this, STATE_CLOSED);
}
}
function checkServer(monitor, callback) {
if (monitor[kConnection] && monitor[kConnection].closed) {
monitor[kConnection] = undefined;
function resetMonitorState(monitor) {
stateTransition(monitor, STATE_CLOSING);
if (monitor[kMonitorId]) {
monitor[kMonitorId].stop();
monitor[kMonitorId] = null;
}
const start = now();
if (monitor[kRTTPinger]) {
monitor[kRTTPinger].close();
monitor[kRTTPinger] = undefined;
}
monitor[kCancellationToken].emit('cancel');
if (monitor[kMonitorId]) {
clearTimeout(monitor[kMonitorId]);
monitor[kMonitorId] = undefined;
}
if (monitor[kConnection]) {
monitor[kConnection].destroy({ force: true });
}
}
function checkServer(monitor, callback) {
let start = now();
monitor.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(monitor.address));
function failureHandler(err) {
if (monitor[kConnection]) {
monitor[kConnection].destroy({ force: true });
monitor[kConnection] = undefined;
}
monitor.emit(
'serverHeartbeatFailed',
new ServerHeartbeatFailedEvent(calculateDurationInMs(start), err, monitor.address)
);
monitor.emit('resetServer', err);
monitor.emit('resetConnectionPool');
callback(err);
}
function successHandler(isMaster) {
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(calculateDurationInMs(start), isMaster, monitor.address)
);
return callback(undefined, isMaster);
}
if (monitor[kConnection] != null) {
if (monitor[kConnection] != null && !monitor[kConnection].closed) {
const connectTimeoutMS = monitor.options.connectTimeoutMS;
monitor[kConnection].command(
'admin.$cmd',
{ ismaster: true },
{ socketTimeout: connectTimeoutMS },
(err, result) => {
if (err) {
failureHandler(err);
return;
const maxAwaitTimeMS = monitor.options.heartbeatFrequencyMS;
const topologyVersion = monitor[kServer].description.topologyVersion;
const isAwaitable = topologyVersion != null;
const cmd = isAwaitable
? { ismaster: true, maxAwaitTimeMS, topologyVersion: makeTopologyVersion(topologyVersion) }
: { ismaster: true };
const options = isAwaitable
? { socketTimeout: connectTimeoutMS + maxAwaitTimeMS, exhaustAllowed: true }
: { socketTimeout: connectTimeoutMS };
if (isAwaitable && monitor[kRTTPinger] == null) {
monitor[kRTTPinger] = new RTTPinger(monitor[kCancellationToken], monitor.connectOptions);
}
monitor[kConnection].command('admin.$cmd', cmd, options, (err, result) => {
if (err) {
failureHandler(err);
return;
}
const isMaster = result.result;
const duration = isAwaitable
? monitor[kRTTPinger].roundTripTime
: calculateDurationInMs(start);
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(duration, isMaster, monitor.address)
);
// if we are using the streaming protocol then we immediately issue another `started`
// event, otherwise the "check" is complete and return to the main monitor loop
if (isAwaitable && isMaster.topologyVersion) {
monitor.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(monitor.address));
start = now();
} else {
if (monitor[kRTTPinger]) {
monitor[kRTTPinger].close();
monitor[kRTTPinger] = undefined;
}
successHandler(result.result);
callback(undefined, isMaster);
}
);
});
return;
}
// connecting does an implicit `ismaster`
connect(monitor.connectOptions, monitor[kCancellationToken], (err, conn) => {
if (conn && isInCloseState(monitor)) {
conn.destroy({ force: true });
return;
}
if (err) {
monitor[kConnection] = undefined;
// we already reset the connection pool on network errors in all cases
if (!(err instanceof MongoNetworkError)) {
monitor.emit('resetConnectionPool');
}
failureHandler(err);
return;
}
if (isInCloseState(monitor)) {
conn.destroy({ force: true });
failureHandler(new MongoError('monitor was destroyed'));
return;
}
monitor[kConnection] = conn;
successHandler(conn.ismaster);
monitor.emit(
'serverHeartbeatSucceeded',
new ServerHeartbeatSucceededEvent(
calculateDurationInMs(start),
conn.ismaster,
monitor.address
)
);
callback(undefined, conn.ismaster);
});
}
@@ -212,33 +293,113 @@ function monitorServer(monitor) {
// TODO: the next line is a legacy event, remove in v4
process.nextTick(() => monitor.emit('monitoring', monitor[kServer]));
checkServer(monitor, e0 => {
if (e0 == null) {
return done();
}
// otherwise an error occured on initial discovery, also bail
if (monitor[kServer].description.type === ServerType.Unknown) {
monitor.emit('resetServer', e0);
return done();
}
// According to the SDAM specification's "Network error during server check" section, if
// an ismaster call fails we reset the server's pool. If a server was once connected,
// change its type to `Unknown` only after retrying once.
monitor.emit('resetConnectionPool');
checkServer(monitor, e1 => {
if (e1) {
monitor.emit('resetServer', e1);
checkServer(monitor, (err, isMaster) => {
if (err) {
// otherwise an error occured on initial discovery, also bail
if (monitor[kServer].description.type === ServerType.Unknown) {
monitor.emit('resetServer', err);
return done();
}
}
done();
});
// if the check indicates streaming is supported, immediately reschedule monitoring
if (isMaster && isMaster.topologyVersion) {
setTimeout(() => {
if (!isInCloseState(monitor)) {
monitor[kMonitorId].wake();
}
});
}
done();
});
};
}
function makeTopologyVersion(tv) {
return {
processId: tv.processId,
counter: BSON.Long.fromNumber(tv.counter)
};
}
class RTTPinger {
constructor(cancellationToken, options) {
this[kConnection] = null;
this[kCancellationToken] = cancellationToken;
this[kRoundTripTime] = 0;
this.closed = false;
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
this[kMonitorId] = setTimeout(() => measureRoundTripTime(this, options), heartbeatFrequencyMS);
}
get roundTripTime() {
return this[kRoundTripTime];
}
close() {
this.closed = true;
clearTimeout(this[kMonitorId]);
this[kMonitorId] = undefined;
if (this[kConnection]) {
this[kConnection].destroy({ force: true });
}
}
}
function measureRoundTripTime(rttPinger, options) {
const start = now();
const cancellationToken = rttPinger[kCancellationToken];
const heartbeatFrequencyMS = options.heartbeatFrequencyMS;
if (rttPinger.closed) {
return;
}
function measureAndReschedule(conn) {
if (rttPinger.closed) {
conn.destroy({ force: true });
return;
}
if (rttPinger[kConnection] == null) {
rttPinger[kConnection] = conn;
}
rttPinger[kRoundTripTime] = calculateDurationInMs(start);
rttPinger[kMonitorId] = setTimeout(
() => measureRoundTripTime(rttPinger, options),
heartbeatFrequencyMS
);
}
if (rttPinger[kConnection] == null) {
connect(options, cancellationToken, (err, conn) => {
if (err) {
rttPinger[kConnection] = undefined;
rttPinger[kRoundTripTime] = 0;
return;
}
measureAndReschedule(conn);
});
return;
}
rttPinger[kConnection].command('admin.$cmd', { ismaster: 1 }, err => {
if (err) {
rttPinger[kConnection] = undefined;
rttPinger[kRoundTripTime] = 0;
return;
}
measureAndReschedule();
});
}
module.exports = {
Monitor
};

View File

@@ -7,17 +7,22 @@ const relayEvents = require('../utils').relayEvents;
const BSON = require('../connection/utils').retrieveBSON();
const Logger = require('../connection/logger');
const ServerDescription = require('./server_description').ServerDescription;
const compareTopologyVersion = require('./server_description').compareTopologyVersion;
const ReadPreference = require('../topologies/read_preference');
const Monitor = require('./monitor').Monitor;
const MongoNetworkError = require('../error').MongoNetworkError;
const MongoNetworkTimeoutError = require('../error').MongoNetworkTimeoutError;
const collationNotSupported = require('../utils').collationNotSupported;
const debugOptions = require('../connection/utils').debugOptions;
const isSDAMUnrecoverableError = require('../error').isSDAMUnrecoverableError;
const isNetworkTimeoutError = require('../error').isNetworkTimeoutError;
const isRetryableWriteError = require('../error').isRetryableWriteError;
const isNodeShuttingDownError = require('../error').isNodeShuttingDownError;
const isNetworkErrorBeforeHandshake = require('../error').isNetworkErrorBeforeHandshake;
const maxWireVersion = require('../utils').maxWireVersion;
const makeStateMachine = require('../utils').makeStateMachine;
const common = require('./common');
const ServerType = common.ServerType;
const isTransactionCommand = require('../transactions').isTransactionCommand;
// Used for filtering out fields for logging
const DEBUG_FIELDS = [
@@ -110,9 +115,8 @@ class Server extends EventEmitter {
// create the connection pool
// NOTE: this used to happen in `connect`, we supported overriding pool options there
const addressParts = this.description.address.split(':');
const poolOptions = Object.assign(
{ host: addressParts[0], port: parseInt(addressParts[1], 10), bson: this.s.bson },
{ host: this.description.host, port: this.description.port, bson: this.s.bson },
options
);
@@ -278,7 +282,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.command(ns, cmd, options, makeOperationHandler(this, options, cb));
conn.command(ns, cmd, options, makeOperationHandler(this, conn, cmd, options, cb));
}, callback);
}
@@ -302,7 +306,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.query(ns, cmd, cursorState, options, makeOperationHandler(this, options, cb));
conn.query(ns, cmd, cursorState, options, makeOperationHandler(this, conn, cmd, options, cb));
}, callback);
}
@@ -326,7 +330,13 @@ class Server extends EventEmitter {
return cb(err);
}
conn.getMore(ns, cursorState, batchSize, options, makeOperationHandler(this, options, cb));
conn.getMore(
ns,
cursorState,
batchSize,
options,
makeOperationHandler(this, conn, null, options, cb)
);
}, callback);
}
@@ -352,7 +362,7 @@ class Server extends EventEmitter {
return cb(err);
}
conn.killCursors(ns, cursorState, makeOperationHandler(this, null, cb));
conn.killCursors(ns, cursorState, makeOperationHandler(this, conn, null, undefined, cb));
}, callback);
}
@@ -414,6 +424,14 @@ Object.defineProperty(Server.prototype, 'clusterTime', {
}
});
function supportsRetryableWrites(server) {
return (
server.description.maxWireVersion >= 6 &&
server.description.logicalSessionTimeoutMinutes &&
server.description.type !== ServerType.Standalone
);
}
function calculateRoundTripTime(oldRtt, duration) {
if (oldRtt === -1) {
return duration;
@@ -448,6 +466,13 @@ function executeWriteOperation(args, options, callback) {
callback(new MongoError(`server ${server.name} does not support collation`));
return;
}
const unacknowledgedWrite = options.writeConcern && options.writeConcern.w === 0;
if (unacknowledgedWrite || maxWireVersion(server) < 5) {
if ((op === 'update' || op === 'remove') && ops.find(o => o.hint)) {
callback(new MongoError(`servers < 3.4 do not support hint on ${op}`));
return;
}
}
server.s.pool.withConnection((err, conn, cb) => {
if (err) {
@@ -455,38 +480,78 @@ function executeWriteOperation(args, options, callback) {
return cb(err);
}
conn[op](ns, ops, options, makeOperationHandler(server, options, cb));
conn[op](ns, ops, options, makeOperationHandler(server, conn, ops, options, cb));
}, callback);
}
function markServerUnknown(server, error) {
if (error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError)) {
server[kMonitor].reset();
}
server.emit(
'descriptionReceived',
new ServerDescription(server.description.address, null, { error })
new ServerDescription(server.description.address, null, {
error,
topologyVersion:
error && error.topologyVersion ? error.topologyVersion : server.description.topologyVersion
})
);
}
function makeOperationHandler(server, options, callback) {
function connectionIsStale(pool, connection) {
return connection.generation !== pool.generation;
}
function shouldHandleStateChangeError(server, err) {
const etv = err.topologyVersion;
const stv = server.description.topologyVersion;
return compareTopologyVersion(stv, etv) < 0;
}
function inActiveTransaction(session, cmd) {
return session && session.inTransaction() && !isTransactionCommand(cmd);
}
function makeOperationHandler(server, connection, cmd, options, callback) {
const session = options && options.session;
return function handleOperationResult(err, result) {
if (err) {
if (err && !connectionIsStale(server.s.pool, connection)) {
if (err instanceof MongoNetworkError) {
if (session && !session.hasEnded) {
session.serverSession.isDirty = true;
}
if (!isNetworkTimeoutError(err)) {
if (supportsRetryableWrites(server) && !inActiveTransaction(session, cmd)) {
err.addErrorLabel('RetryableWriteError');
}
if (!(err instanceof MongoNetworkTimeoutError) || isNetworkErrorBeforeHandshake(err)) {
markServerUnknown(server, err);
server.s.pool.clear();
}
} else if (isSDAMUnrecoverableError(err)) {
if (maxWireVersion(server) <= 7 || isNodeShuttingDownError(err)) {
server.s.pool.clear();
} else {
// if pre-4.4 server, then add error label if its a retryable write error
if (
maxWireVersion(server) < 9 &&
isRetryableWriteError(err) &&
!inActiveTransaction(session, cmd)
) {
err.addErrorLabel('RetryableWriteError');
}
markServerUnknown(server, err);
process.nextTick(() => server.requestCheck());
if (isSDAMUnrecoverableError(err)) {
if (shouldHandleStateChangeError(server, err)) {
if (maxWireVersion(server) <= 7 || isNodeShuttingDownError(err)) {
server.s.pool.clear();
}
markServerUnknown(server, err);
process.nextTick(() => server.requestCheck());
}
}
}
}

View File

@@ -53,6 +53,8 @@ class ServerDescription {
* @param {Object} [ismaster] An optional ismaster response for this server
* @param {Object} [options] Optional settings
* @param {Number} [options.roundTripTime] The round trip time to ping this server (in ms)
* @param {Error} [options.error] An Error used for better reporting debugging
* @param {any} [options.topologyVersion] The topologyVersion
*/
constructor(address, ismaster, options) {
options = options || {};
@@ -75,6 +77,7 @@ class ServerDescription {
this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
this.opTime = ismaster.lastWrite ? ismaster.lastWrite.opTime : null;
this.type = parseServerType(ismaster);
this.topologyVersion = options.topologyVersion || ismaster.topologyVersion;
// direct mappings
ISMASTER_FIELDS.forEach(field => {
@@ -113,6 +116,16 @@ class ServerDescription {
return WRITABLE_SERVER_TYPES.has(this.type);
}
get host() {
const chopLength = `:${this.port}`.length;
return this.address.slice(0, -chopLength);
}
get port() {
const port = this.address.split(':').pop();
return port ? Number.parseInt(port, 10) : port;
}
/**
* Determines if another `ServerDescription` is equal to this one per the rules defined
* in the {@link https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#serverdescription|SDAM spec}
@@ -121,6 +134,10 @@ class ServerDescription {
* @return {Boolean}
*/
equals(other) {
const topologyVersionsEqual =
this.topologyVersion === other.topologyVersion ||
compareTopologyVersion(this.topologyVersion, other.topologyVersion) === 0;
return (
other != null &&
errorStrictEqual(this.error, other.error) &&
@@ -135,7 +152,8 @@ class ServerDescription {
? other.electionId && this.electionId.equals(other.electionId)
: this.electionId === other.electionId) &&
this.primary === other.primary &&
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes &&
topologyVersionsEqual
);
}
}
@@ -176,7 +194,34 @@ function parseServerType(ismaster) {
return ServerType.Standalone;
}
/**
* Compares two topology versions.
*
* @param {object} lhs
* @param {object} rhs
* @returns A negative number if `lhs` is older than `rhs`; positive if `lhs` is newer than `rhs`; 0 if they are equivalent.
*/
function compareTopologyVersion(lhs, rhs) {
if (lhs == null || rhs == null) {
return -1;
}
if (lhs.processId.equals(rhs.processId)) {
// TODO: handle counters as Longs
if (lhs.counter === rhs.counter) {
return 0;
} else if (lhs.counter < rhs.counter) {
return -1;
}
return 1;
}
return -1;
}
module.exports = {
ServerDescription,
parseServerType
parseServerType,
compareTopologyVersion
};

View File

@@ -14,7 +14,6 @@ const CoreCursor = require('../cursor').CoreCursor;
const deprecate = require('util').deprecate;
const BSON = require('../connection/utils').retrieveBSON();
const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
const isRetryableError = require('../error').isRetryableError;
const ClientSession = require('../sessions').ClientSession;
const MongoError = require('../error').MongoError;
const MongoServerSelectionError = require('../error').MongoServerSelectionError;
@@ -27,6 +26,7 @@ const emitDeprecationWarning = require('../../utils').emitDeprecationWarning;
const ServerSessionPool = require('../sessions').ServerSessionPool;
const makeClientMetadata = require('../utils').makeClientMetadata;
const CMAP_EVENT_NAMES = require('../../cmap/events').CMAP_EVENT_NAMES;
const compareTopologyVersion = require('./server_description').compareTopologyVersion;
const common = require('./common');
const drainTimerQueue = common.drainTimerQueue;
@@ -275,9 +275,9 @@ class Topology extends EventEmitter {
// connect all known servers, then attempt server selection to connect
connectServers(this, Array.from(this.s.description.servers.values()));
translateReadPreference(options);
ReadPreference.translate(options);
const readPreference = options.readPreference || ReadPreference.primary;
this.selectServer(readPreferenceServerSelector(readPreference), options, err => {
const connectHandler = err => {
if (err) {
this.close();
@@ -295,7 +295,15 @@ class Topology extends EventEmitter {
this.emit('connect', this);
if (typeof callback === 'function') callback(err, this);
});
};
// TODO: NODE-2471
if (this.s.credentials) {
this.command('admin.$cmd', { ping: 1 }, { readPreference }, connectHandler);
return;
}
this.selectServer(readPreferenceServerSelector(readPreference), options, connectHandler);
}
/**
@@ -381,7 +389,7 @@ class Topology extends EventEmitter {
} else if (typeof selector === 'string') {
readPreference = new ReadPreference(selector);
} else {
translateReadPreference(options);
ReadPreference.translate(options);
readPreference = options.readPreference || ReadPreference.primary;
}
@@ -505,6 +513,11 @@ class Topology extends EventEmitter {
return;
}
// ignore this server update if its from an outdated topologyVersion
if (isStaleServerDescription(this.s.description, serverDescription)) {
return;
}
// these will be used for monitoring events later
const previousTopologyDescription = this.s.description;
const previousServerDescription = this.s.description.servers.get(serverDescription.address);
@@ -647,7 +660,7 @@ class Topology extends EventEmitter {
(callback = options), (options = {}), (options = options || {});
}
translateReadPreference(options);
ReadPreference.translate(options);
const readPreference = options.readPreference || ReadPreference.primary;
this.selectServer(readPreferenceServerSelector(readPreference), options, (err, server) => {
@@ -666,7 +679,7 @@ class Topology extends EventEmitter {
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!shouldRetryOperation(err)) {
return callback(err);
}
@@ -708,7 +721,7 @@ class Topology extends EventEmitter {
options = options || {};
const topology = options.topology || this;
const CursorClass = options.cursorFactory || this.s.Cursor;
translateReadPreference(options);
ReadPreference.translate(options);
return new CursorClass(topology, ns, cmd, options);
}
@@ -771,6 +784,16 @@ function isWriteCommand(command) {
return RETRYABLE_WRITE_OPERATIONS.some(op => command[op]);
}
function isStaleServerDescription(topologyDescription, incomingServerDescription) {
const currentServerDescription = topologyDescription.servers.get(
incomingServerDescription.address
);
const currentTopologyVersion = currentServerDescription.topologyVersion;
return (
compareTopologyVersion(currentTopologyVersion, incomingServerDescription.topologyVersion) > 0
);
}
/**
* Destroys a server, and removes all event listeners from the instance
*
@@ -806,10 +829,16 @@ function parseStringSeedlist(seedlist) {
}
function topologyTypeFromSeedlist(seedlist, options) {
if (options.directConnection) {
return TopologyType.Single;
}
const replicaSet = options.replicaSet || options.setName || options.rs_name;
if (seedlist.length === 1 && !replicaSet) return TopologyType.Single;
if (replicaSet) return TopologyType.ReplicaSetNoPrimary;
return TopologyType.Unknown;
if (replicaSet == null) {
return TopologyType.Unknown;
}
return TopologyType.ReplicaSetNoPrimary;
}
function randomSelection(array) {
@@ -911,7 +940,7 @@ function executeWriteOperation(args, options, callback) {
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!shouldRetryOperation(err)) {
err = getMMAPError(err);
return callback(err);
}
@@ -939,26 +968,8 @@ function executeWriteOperation(args, options, callback) {
});
}
function translateReadPreference(options) {
if (options.readPreference == null) {
return;
}
let r = options.readPreference;
if (typeof r === 'string') {
options.readPreference = new ReadPreference(r);
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
const mode = r.mode || r.preference;
if (mode && typeof mode === 'string') {
options.readPreference = new ReadPreference(mode, r.tags, {
maxStalenessSeconds: r.maxStalenessSeconds
});
}
} else if (!(r instanceof ReadPreference)) {
throw new TypeError('Invalid read preference: ' + r);
}
return options;
function shouldRetryOperation(err) {
return err instanceof MongoError && err.hasErrorLabel('RetryableWriteError');
}
function srvPollingHandler(topology) {

View File

@@ -132,6 +132,10 @@ class TopologyDescription {
let maxElectionId = this.maxElectionId;
let commonWireVersion = this.commonWireVersion;
if (serverDescription.setName && setName && serverDescription.setName !== setName) {
serverDescription = new ServerDescription(address, null);
}
const serverType = serverDescription.type;
let serverDescriptions = new Map(this.servers);
@@ -161,7 +165,7 @@ class TopologyDescription {
}
if (topologyType === TopologyType.Unknown) {
if (serverType === ServerType.Standalone) {
if (serverType === ServerType.Standalone && this.servers.size !== 1) {
serverDescriptions.delete(address);
} else {
topologyType = topologyTypeForServerType(serverType);
@@ -274,8 +278,22 @@ class TopologyDescription {
}
function topologyTypeForServerType(serverType) {
if (serverType === ServerType.Mongos) return TopologyType.Sharded;
if (serverType === ServerType.RSPrimary) return TopologyType.ReplicaSetWithPrimary;
if (serverType === ServerType.Standalone) {
return TopologyType.Single;
}
if (serverType === ServerType.Mongos) {
return TopologyType.Sharded;
}
if (serverType === ServerType.RSPrimary) {
return TopologyType.ReplicaSetWithPrimary;
}
if (serverType === ServerType.RSGhost || serverType === ServerType.Unknown) {
return TopologyType.Unknown;
}
return TopologyType.ReplicaSetNoPrimary;
}

View File

@@ -386,10 +386,7 @@ function attemptTransaction(session, startTime, fn, options) {
}
if (isMaxTimeMSExpiredError(err)) {
if (err.errorLabels == null) {
err.errorLabels = [];
}
err.errorLabels.push('UnknownTransactionCommitResult');
err.addErrorLabel('UnknownTransactionCommitResult');
}
throw err;
@@ -488,17 +485,8 @@ function endTransaction(session, commandName, callback) {
isRetryableError(e) ||
isMaxTimeMSExpiredError(e))
) {
if (e.errorLabels) {
const idx = e.errorLabels.indexOf('TransientTransactionError');
if (idx !== -1) {
e.errorLabels.splice(idx, 1);
}
} else {
e.errorLabels = [];
}
if (isUnknownTransactionCommitResult(e)) {
e.errorLabels.push('UnknownTransactionCommitResult');
e.addErrorLabel('UnknownTransactionCommitResult');
// per txns spec, must unpin session in this case
session.transaction.unpinServer();
@@ -685,7 +673,12 @@ function commandSupportsReadConcern(command, options) {
return true;
}
if (command.mapReduce && options.out && (options.out.inline === 1 || options.out === 'inline')) {
if (
command.mapReduce &&
options &&
options.out &&
(options.out.inline === 1 || options.out === 'inline')
) {
return true;
}
@@ -708,6 +701,11 @@ function applySession(session, command, options) {
return new MongoError('Cannot use a session that has ended');
}
// SPEC-1019: silently ignore explicit session with unacknowledged write for backwards compatibility
if (options && options.writeConcern && options.writeConcern.w === 0) {
return;
}
const serverSession = session.serverSession;
serverSession.lastUse = now();
command.lsid = serverSession.id;
@@ -715,7 +713,7 @@ function applySession(session, command, options) {
// first apply non-transaction-specific sessions data
const inTransaction = session.inTransaction() || isTransactionCommand(command);
const isRetryableWrite = options.willRetryWrite;
const shouldApplyReadConcern = commandSupportsReadConcern(command);
const shouldApplyReadConcern = commandSupportsReadConcern(command, options);
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);

View File

@@ -13,10 +13,10 @@ const cloneOptions = require('./shared').cloneOptions;
const SessionMixins = require('./shared').SessionMixins;
const isRetryableWritesSupported = require('./shared').isRetryableWritesSupported;
const relayEvents = require('../utils').relayEvents;
const isRetryableError = require('../error').isRetryableError;
const BSON = retrieveBSON();
const getMMAPError = require('./shared').getMMAPError;
const makeClientMetadata = require('../utils').makeClientMetadata;
const legacyIsRetryableWriteError = require('./shared').legacyIsRetryableWriteError;
/**
* @fileOverview The **Mongos** class is a class that represents a Mongos Proxy topology and is
@@ -71,7 +71,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {number} [options.localThresholdMS=15] Cutoff latency point in MS for MongoS proxy selection
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=1000] TCP Connection timeout setting
@@ -113,6 +113,18 @@ var Mongos = function(seedlist, options) {
// Get replSet Id
this.id = id++;
// deduplicate seedlist
if (Array.isArray(seedlist)) {
seedlist = seedlist.reduce((seeds, seed) => {
if (seeds.find(s => s.host === seed.host && s.port === seed.port)) {
return seeds;
}
seeds.push(seed);
return seeds;
}, []);
}
// Internal state
this.s = {
options: Object.assign({ metadata: makeClientMetadata(options) }, options),
@@ -911,7 +923,7 @@ function executeWriteOperation(args, options, callback) {
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err) || !willRetryWrite) {
if (!legacyIsRetryableWriteError(err, self) || !willRetryWrite) {
err = getMMAPError(err);
return callback(err);
}
@@ -1107,7 +1119,7 @@ Mongos.prototype.command = function(ns, cmd, options, callback) {
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
return callback(err);
}
@@ -1121,8 +1133,8 @@ Mongos.prototype.command = function(ns, cmd, options, callback) {
// increment and assign txnNumber
if (willRetryWrite) {
options.session.incrementTransactionNumber();
options.willRetryWrite = willRetryWrite;
clonedOptions.session.incrementTransactionNumber();
clonedOptions.willRetryWrite = willRetryWrite;
}
// Execute the command

View File

@@ -8,6 +8,8 @@
* @param {array} tags The tags object
* @param {object} [options] Additional read preference options
* @param {number} [options.maxStalenessSeconds] Max secondary read staleness in seconds, Minimum value is 90 seconds.
* @param {object} [options.hedge] Server mode in which the same query is dispatched in parallel to multiple replica set members.
* @param {boolean} [options.hedge.enabled] Explicitly enable or disable hedged reads.
* @see https://docs.mongodb.com/manual/core/read-preference/
* @return {ReadPreference}
*/
@@ -22,7 +24,10 @@ const ReadPreference = function(mode, tags, options) {
'ReadPreference tags must be an array, this will change in the next major version'
);
if (typeof tags.maxStalenessSeconds !== 'undefined') {
const tagsHasMaxStalenessSeconds = typeof tags.maxStalenessSeconds !== 'undefined';
const tagsHasHedge = typeof tags.hedge !== 'undefined';
const tagsHasOptions = tagsHasMaxStalenessSeconds || tagsHasHedge;
if (tagsHasOptions) {
// this is likely an options object
options = tags;
tags = undefined;
@@ -33,6 +38,7 @@ const ReadPreference = function(mode, tags, options) {
this.mode = mode;
this.tags = tags;
this.hedge = options && options.hedge;
options = options || {};
if (options.maxStalenessSeconds != null) {
@@ -55,6 +61,10 @@ const ReadPreference = function(mode, tags, options) {
if (this.maxStalenessSeconds) {
throw new TypeError('Primary read preference cannot be combined with maxStalenessSeconds');
}
if (this.hedge) {
throw new TypeError('Primary read preference cannot be combined with hedge');
}
}
};
@@ -91,20 +101,19 @@ const VALID_MODES = [
* @return {ReadPreference}
*/
ReadPreference.fromOptions = function(options) {
if (!options) return null;
const readPreference = options.readPreference;
if (!readPreference) return null;
const readPreferenceTags = options.readPreferenceTags;
if (readPreference == null) {
return null;
}
const maxStalenessSeconds = options.maxStalenessSeconds;
if (typeof readPreference === 'string') {
return new ReadPreference(readPreference, readPreferenceTags);
} else if (!(readPreference instanceof ReadPreference) && typeof readPreference === 'object') {
const mode = readPreference.mode || readPreference.preference;
if (mode && typeof mode === 'string') {
return new ReadPreference(mode, readPreference.tags, {
maxStalenessSeconds: readPreference.maxStalenessSeconds
maxStalenessSeconds: readPreference.maxStalenessSeconds || maxStalenessSeconds,
hedge: readPreference.hedge
});
}
}
@@ -112,6 +121,60 @@ ReadPreference.fromOptions = function(options) {
return readPreference;
};
/**
* Resolves a read preference based on well-defined inheritance rules. This method will not only
* determine the read preference (if there is one), but will also ensure the returned value is a
* properly constructed instance of `ReadPreference`.
*
* @param {Collection|Db|MongoClient} parent The parent of the operation on which to determine the read
* preference, used for determining the inherited read preference.
* @param {object} options The options passed into the method, potentially containing a read preference
* @returns {(ReadPreference|null)} The resolved read preference
*/
ReadPreference.resolve = function(parent, options) {
options = options || {};
const session = options.session;
const inheritedReadPreference = parent && parent.readPreference;
let readPreference;
if (options.readPreference) {
readPreference = ReadPreference.fromOptions(options);
} else if (session && session.inTransaction() && session.transaction.options.readPreference) {
// The transactions read preference MUST override all other user configurable read preferences.
readPreference = session.transaction.options.readPreference;
} else if (inheritedReadPreference != null) {
readPreference = inheritedReadPreference;
} else {
readPreference = ReadPreference.primary;
}
return typeof readPreference === 'string' ? new ReadPreference(readPreference) : readPreference;
};
/**
* Replaces options.readPreference with a ReadPreference instance
*/
ReadPreference.translate = function(options) {
if (options.readPreference == null) return options;
const r = options.readPreference;
if (typeof r === 'string') {
options.readPreference = new ReadPreference(r);
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
const mode = r.mode || r.preference;
if (mode && typeof mode === 'string') {
options.readPreference = new ReadPreference(mode, r.tags, {
maxStalenessSeconds: r.maxStalenessSeconds
});
}
} else if (!(r instanceof ReadPreference)) {
throw new TypeError('Invalid read preference: ' + r);
}
return options;
};
/**
* Validate if a mode is legal
*
@@ -165,6 +228,7 @@ ReadPreference.prototype.toJSON = function() {
const readPreference = { mode: this.mode };
if (Array.isArray(this.tags)) readPreference.tags = this.tags;
if (this.maxStalenessSeconds) readPreference.maxStalenessSeconds = this.maxStalenessSeconds;
if (this.hedge) readPreference.hedge = this.hedge;
return readPreference;
};

View File

@@ -15,10 +15,10 @@ const Interval = require('./shared').Interval;
const SessionMixins = require('./shared').SessionMixins;
const isRetryableWritesSupported = require('./shared').isRetryableWritesSupported;
const relayEvents = require('../utils').relayEvents;
const isRetryableError = require('../error').isRetryableError;
const BSON = retrieveBSON();
const getMMAPError = require('./shared').getMMAPError;
const makeClientMetadata = require('../utils').makeClientMetadata;
const legacyIsRetryableWriteError = require('./shared').legacyIsRetryableWriteError;
const now = require('../../utils').now;
const calculateDurationInMs = require('../../utils').calculateDurationInMs;
@@ -72,7 +72,7 @@ var handlers = ['connect', 'close', 'error', 'timeout', 'parseError'];
* @param {Cursor} [options.cursorFactory=Cursor] The cursor factory class used for all query cursors
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=0] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=10000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=0] TCP Socket timeout setting
@@ -1202,7 +1202,7 @@ function executeWriteOperation(args, options, callback) {
const handler = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
err = getMMAPError(err);
return callback(err);
}
@@ -1365,7 +1365,7 @@ ReplSet.prototype.command = function(ns, cmd, options, callback) {
const cb = (err, result) => {
if (!err) return callback(null, result);
if (!isRetryableError(err)) {
if (!legacyIsRetryableWriteError(err, self)) {
return callback(err);
}

View File

@@ -72,7 +72,7 @@ function topologyId(server) {
* @param {number} options.port The server port
* @param {number} [options.size=5] Server connection pool size
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=300000] Initial delay before TCP keep alive enabled
* @param {number} [options.keepAliveInitialDelay=120000] Initial delay before TCP keep alive enabled
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {number} [options.connectionTimeout=30000] TCP Connection timeout setting
* @param {number} [options.socketTimeout=360000] TCP Socket timeout setting

View File

@@ -2,7 +2,9 @@
const ReadPreference = require('./read_preference');
const TopologyType = require('../sdam/common').TopologyType;
const MongoError = require('../error').MongoError;
const isRetryableWriteError = require('../error').isRetryableWriteError;
const maxWireVersion = require('../utils').maxWireVersion;
const MongoNetworkError = require('../error').MongoNetworkError;
const MMAPv1_RETRY_WRITES_ERROR_CODE = 20;
/**
@@ -416,18 +418,39 @@ function getMMAPError(err) {
return newErr;
}
module.exports.SessionMixins = SessionMixins;
module.exports.resolveClusterTime = resolveClusterTime;
module.exports.inquireServerState = inquireServerState;
module.exports.getTopologyType = getTopologyType;
module.exports.emitServerDescriptionChanged = emitServerDescriptionChanged;
module.exports.emitTopologyDescriptionChanged = emitTopologyDescriptionChanged;
module.exports.cloneOptions = cloneOptions;
module.exports.createCompressionInfo = createCompressionInfo;
module.exports.clone = clone;
module.exports.diff = diff;
module.exports.Interval = Interval;
module.exports.Timeout = Timeout;
module.exports.isRetryableWritesSupported = isRetryableWritesSupported;
module.exports.getMMAPError = getMMAPError;
module.exports.topologyType = topologyType;
// NOTE: only used for legacy topology types
function legacyIsRetryableWriteError(err, topology) {
if (!(err instanceof MongoError)) {
return false;
}
// if pre-4.4 server, then add error label if its a retryable write error
if (
isRetryableWritesSupported(topology) &&
(err instanceof MongoNetworkError ||
(maxWireVersion(topology) < 9 && isRetryableWriteError(err)))
) {
err.addErrorLabel('RetryableWriteError');
}
return err.hasErrorLabel('RetryableWriteError');
}
module.exports = {
SessionMixins,
resolveClusterTime,
inquireServerState,
getTopologyType,
emitServerDescriptionChanged,
emitTopologyDescriptionChanged,
cloneOptions,
createCompressionInfo,
clone,
diff,
Interval,
Timeout,
isRetryableWritesSupported,
getMMAPError,
topologyType,
legacyIsRetryableWriteError
};

View File

@@ -37,6 +37,10 @@ function matchesParentDomain(srvAddress, parentDomain) {
function parseSrvConnectionString(uri, options, callback) {
const result = URL.parse(uri, true);
if (options.directConnection || options.directconnection) {
return callback(new MongoParseError('directConnection not supported with SRV URI'));
}
if (result.hostname.split('.').length < 3) {
return callback(new MongoParseError('URI does not have hostname, domain name and tld'));
}
@@ -171,6 +175,7 @@ const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
// NOTE: this list exists in native already, if it is merged here we should deduplicate
const AUTH_MECHANISMS = new Set([
'GSSAPI',
'MONGODB-AWS',
'MONGODB-X509',
'MONGODB-CR',
'DEFAULT',
@@ -214,7 +219,8 @@ const CASE_TRANSLATION = {
tlscertificatekeyfile: 'tlsCertificateKeyFile',
tlscertificatekeyfilepassword: 'tlsCertificateKeyFilePassword',
wtimeout: 'wTimeoutMS',
j: 'journal'
j: 'journal',
directconnection: 'directConnection'
};
/**
@@ -257,7 +263,9 @@ function applyConnectionStringOption(obj, key, value, options) {
if (key === 'authmechanism' && !AUTH_MECHANISMS.has(value)) {
throw new MongoParseError(
'Value for `authMechanism` must be one of: `DEFAULT`, `GSSAPI`, `PLAIN`, `MONGODB-X509`, `SCRAM-SHA-1`, `SCRAM-SHA-256`'
`Value for authMechanism must be one of: ${Array.from(AUTH_MECHANISMS).join(
', '
)}, found: ${value}`
);
}
@@ -357,6 +365,16 @@ function applyAuthExpectations(parsed) {
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
if (authMechanism === 'MONGODB-AWS') {
if (authSource != null && authSource !== '$external') {
throw new MongoParseError(
`Invalid source \`${authSource}\` for mechanism \`${authMechanism}\` specified.`
);
}
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
}
if (authMechanism === 'MONGODB-X509') {
if (parsed.auth && parsed.auth.password != null) {
throw new MongoParseError(`Password not allowed for mechanism \`${authMechanism}\``);
@@ -552,10 +570,6 @@ function parseConnectionString(uri, options, callback) {
return callback(new MongoParseError('Invalid protocol provided'));
}
if (protocol === PROTOCOL_MONGODB_SRV) {
return parseSrvConnectionString(uri, options, callback);
}
const dbAndQuery = cap[4].split('?');
const db = dbAndQuery.length > 0 ? dbAndQuery[0] : null;
const query = dbAndQuery.length > 1 ? dbAndQuery[1] : null;
@@ -568,6 +582,11 @@ function parseConnectionString(uri, options, callback) {
}
parsedOptions = Object.assign({}, parsedOptions, options);
if (protocol === PROTOCOL_MONGODB_SRV) {
return parseSrvConnectionString(uri, parsedOptions, callback);
}
const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null };
if (parsedOptions.auth) {
// maintain support for legacy options passed into `MongoClient`
@@ -661,6 +680,22 @@ function parseConnectionString(uri, options, callback) {
return callback(new MongoParseError('No hostname or hostnames provided in connection string'));
}
const directConnection = !!parsedOptions.directConnection;
if (directConnection && hosts.length !== 1) {
// If the option is set to true, the driver MUST validate that there is exactly one host given
// in the host list in the URI, and fail client creation otherwise.
return callback(new MongoParseError('directConnection option requires exactly one host'));
}
// NOTE: this behavior will go away in v4.0, we will always auto discover there
if (
parsedOptions.directConnection == null &&
hosts.length === 1 &&
parsedOptions.replicaSet == null
) {
parsedOptions.directConnection = true;
}
const result = {
hosts: hosts,
auth: auth.db || auth.username ? auth : null,

View File

@@ -149,6 +149,35 @@ function eachAsync(arr, eachFn, callback) {
}
}
function eachAsyncSeries(arr, eachFn, callback) {
arr = arr || [];
let idx = 0;
let awaiting = arr.length;
if (awaiting === 0) {
callback();
return;
}
function eachCallback(err) {
idx++;
awaiting--;
if (err) {
callback(err);
return;
}
if (idx === arr.length && awaiting <= 0) {
callback();
return;
}
eachFn(arr[idx], eachCallback);
}
eachFn(arr[idx], eachCallback);
}
function isUnifiedTopology(topology) {
return topology.description != null;
}
@@ -257,6 +286,7 @@ module.exports = {
maxWireVersion,
isPromiseLike,
eachAsync,
eachAsyncSeries,
isUnifiedTopology,
arrayStrictEqual,
tagsStrictEqual,

View File

@@ -66,12 +66,7 @@ function _command(server, ns, cmd, options, callback) {
finalCmd.$clusterTime = clusterTime;
}
if (
isSharded(server) &&
!shouldUseOpMsg &&
readPreference &&
readPreference.preference !== 'primary'
) {
if (isSharded(server) && !shouldUseOpMsg && readPreference && readPreference.mode !== 'primary') {
finalCmd = {
$query: finalCmd,
$readPreference: readPreference.toJSON()
@@ -105,10 +100,7 @@ function _command(server, ns, cmd, options, callback) {
err instanceof MongoNetworkError &&
!err.hasErrorLabel('TransientTransactionError')
) {
if (err.errorLabels == null) {
err.errorLabels = [];
}
err.errorLabels.push('TransientTransactionError');
err.addErrorLabel('TransientTransactionError');
}
if (

View File

@@ -1,9 +1,9 @@
'use strict';
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
const MAX_SUPPORTED_SERVER_VERSION = '4.2';
const MAX_SUPPORTED_SERVER_VERSION = '4.4';
const MIN_SUPPORTED_WIRE_VERSION = 2;
const MAX_SUPPORTED_WIRE_VERSION = 8;
const MAX_SUPPORTED_WIRE_VERSION = 9;
module.exports = {
MIN_SUPPORTED_SERVER_VERSION,

View File

@@ -100,6 +100,10 @@ function prepareFindCommand(server, ns, cmd, cursorState) {
sortValue = sortObject;
}
if (typeof cmd.allowDiskUse === 'boolean') {
findCmd.allowDiskUse = cmd.allowDiskUse;
}
if (cmd.sort) findCmd.sort = sortValue;
if (cmd.fields) findCmd.projection = cmd.fields;
if (cmd.hint) findCmd.hint = cmd.hint;

1
node_modules/mongodb/lib/cursor.js generated vendored
View File

@@ -81,7 +81,6 @@ const fields = ['numberOfRetries', 'tailableRetryInterval'];
* collection.find({}).filter({a:1}) // Set query on the cursor
* collection.find({}).comment('add a comment') // Add a comment to the query, allowing to correlate queries
* collection.find({}).addCursorFlag('tailable', true) // Set cursor as tailable
* collection.find({}).addCursorFlag('oplogReplay', true) // Set cursor as oplogReplay
* collection.find({}).addCursorFlag('noCursorTimeout', true) // Set cursor as noCursorTimeout
* collection.find({}).addCursorFlag('awaitData', true) // Set cursor as awaitData
* collection.find({}).addCursorFlag('partial', true) // Set cursor as partial

13
node_modules/mongodb/lib/db.js generated vendored
View File

@@ -14,7 +14,6 @@ const Logger = require('./core').Logger;
const Collection = require('./collection');
const mergeOptionsAndWriteConcern = require('./utils').mergeOptionsAndWriteConcern;
const executeLegacyOperation = require('./utils').executeLegacyOperation;
const resolveReadPreference = require('./utils').resolveReadPreference;
const ChangeStream = require('./change_stream');
const deprecate = require('util').deprecate;
const deprecateOptions = require('./utils').deprecateOptions;
@@ -35,8 +34,9 @@ const AggregateOperation = require('./operations/aggregate');
const AddUserOperation = require('./operations/add_user');
const CollectionsOperation = require('./operations/collections');
const CommandOperation = require('./operations/command');
const RunCommandOperation = require('./operations/run_command');
const CreateCollectionOperation = require('./operations/create_collection');
const CreateIndexOperation = require('./operations/create_index');
const CreateIndexesOperation = require('./operations/create_indexes');
const DropCollectionOperation = require('./operations/drop').DropCollectionOperation;
const DropDatabaseOperation = require('./operations/drop').DropDatabaseOperation;
const ExecuteDbAdminCommandOperation = require('./operations/execute_db_admin_command');
@@ -290,7 +290,7 @@ Db.prototype.command = function(command, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = Object.assign({}, options);
const commandOperation = new CommandOperation(this, options, null, command);
const commandOperation = new RunCommandOperation(this, command, options);
return executeOperation(this.s.topology, commandOperation, callback);
};
@@ -709,7 +709,7 @@ Db.prototype.collections = function(options, callback) {
Db.prototype.executeDbAdminCommand = function(selector, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options || {};
options.readPreference = resolveReadPreference(this, options);
options.readPreference = ReadPreference.resolve(this, options);
const executeDbAdminCommandOperation = new ExecuteDbAdminCommandOperation(
this,
@@ -740,6 +740,7 @@ Db.prototype.executeDbAdminCommand = function(selector, options, callback) {
* @param {string} [options.name] Override the autogenerated index name (useful if the resulting name is larger than 128 bytes)
* @param {object} [options.partialFilterExpression] Creates a partial index based on the given filter object (MongoDB 3.2 or higher)
* @param {ClientSession} [options.session] optional session to use for this operation
* @param {(number|string)} [options.commitQuorum] (MongoDB 4.4. or higher) Specifies how many data-bearing members of a replica set, including the primary, must complete the index builds successfully before the primary marks the indexes as ready. This option accepts the same values for the "w" field in a write concern plus "votingMembers", which indicates all voting data-bearing nodes.
* @param {Db~resultCallback} [callback] The command result callback
* @return {Promise} returns Promise if no callback passed
*/
@@ -747,9 +748,9 @@ Db.prototype.createIndex = function(name, fieldOrSpec, options, callback) {
if (typeof options === 'function') (callback = options), (options = {});
options = options ? Object.assign({}, options) : {};
const createIndexOperation = new CreateIndexOperation(this, name, fieldOrSpec, options);
const createIndexesOperation = new CreateIndexesOperation(this, name, fieldOrSpec, options);
return executeOperation(this.s.topology, createIndexOperation, callback);
return executeOperation(this.s.topology, createIndexesOperation, callback);
};
/**

7
node_modules/mongodb/lib/error.js generated vendored
View File

@@ -20,7 +20,8 @@ const GET_MORE_RESUMABLE_CODES = new Set([
150, // StaleEpoch
13388, // StaleConfig
234, // RetryChangeStream
133 // FailedToSatisfyReadPreference
133, // FailedToSatisfyReadPreference
43 // CursorNotFound
]);
function isResumableError(error, wireVersion) {
@@ -29,6 +30,10 @@ function isResumableError(error, wireVersion) {
}
if (wireVersion >= 9) {
// DRIVERS-1308: For 4.4 drivers running against 4.4 servers, drivers will add a special case to treat the CursorNotFound error code as resumable
if (error.code === 43) {
return true;
}
return error.hasErrorLabel('ResumableChangeStreamError');
}

View File

@@ -278,6 +278,7 @@ function init(self) {
if (error) {
return __handleError(self, error);
}
if (!doc) {
var identifier = self.s.filter._id ? self.s.filter._id.toString() : self.s.filter.filename;
var errmsg = 'FileNotFound: file ' + identifier + ' was not found';
@@ -301,7 +302,11 @@ function init(self) {
return;
}
self.s.bytesToSkip = handleStartOption(self, doc, self.s.options);
try {
self.s.bytesToSkip = handleStartOption(self, doc, self.s.options);
} catch (error) {
return __handleError(self, error);
}
var filter = { files_id: doc._id };
@@ -322,7 +327,13 @@ function init(self) {
self.s.expectedEnd = Math.ceil(doc.length / doc.chunkSize);
self.s.file = doc;
self.s.bytesToTrim = handleEndOption(self, doc, self.s.cursor, self.s.options);
try {
self.s.bytesToTrim = handleEndOption(self, doc, self.s.cursor, self.s.options);
} catch (error) {
return __handleError(self, error);
}
self.emit('file', doc);
});
}

View File

@@ -95,7 +95,7 @@ const validOptions = require('./operations/connect').validOptions;
* @param {boolean} [options.autoReconnect=true] Enable autoReconnect for single server instances
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=30000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.keepAliveInitialDelay=120000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.connectTimeoutMS=10000] How long to wait for a connection to be established before timing out
* @param {number} [options.socketTimeoutMS=360000] How long a send or receive on a socket can take before timing out
* @param {number} [options.family] Version of IP stack. Can be 4, 6 or null (default).
@@ -133,7 +133,7 @@ const validOptions = require('./operations/connect').validOptions;
* @param {string} [options.appname=undefined] The name of the application that created this MongoClient instance. MongoDB 3.4 and newer will print this value in the server log upon establishing each connection. It is also recorded in the slow query log and profile collections
* @param {string} [options.auth.user=undefined] The username for auth
* @param {string} [options.auth.password=undefined] The password for auth
* @param {string} [options.authMechanism=undefined] Mechanism for authentication: MDEFAULT, GSSAPI, PLAIN, MONGODB-X509, or SCRAM-SHA-1
* @param {string} [options.authMechanism] An authentication mechanism to use for connection authentication, see the {@link https://docs.mongodb.com/manual/reference/connection-string/#urioption.authMechanism|authMechanism} reference for supported options.
* @param {object} [options.compression] Type of compression to use: snappy or zlib
* @param {boolean} [options.fsync=false] Specify a file sync write concern
* @param {array} [options.readPreferenceTags] Read preference tags
@@ -152,6 +152,7 @@ const validOptions = require('./operations/connect').validOptions;
* @param {number} [options.waitQueueTimeoutMS=0] **Only applies to the unified topology** The maximum amount of time operation execution should wait for a connection to become available. The default is 0 which means there is no limit.
* @param {AutoEncrypter~AutoEncryptionOptions} [options.autoEncryption] Optionally enable client side auto encryption
* @param {DriverInfoOptions} [options.driverInfo] Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver
* @param {boolean} [options.directConnection=false] Enable directConnection
* @param {MongoClient~connectCallback} [callback] The command result callback
* @return {MongoClient} a MongoClient instance
*/
@@ -368,7 +369,7 @@ MongoClient.prototype.isConnected = function(options) {
* @param {boolean} [options.autoReconnect=true] Enable autoReconnect for single server instances
* @param {boolean} [options.noDelay=true] TCP Connection no delay
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.keepAliveInitialDelay=30000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.keepAliveInitialDelay=120000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.connectTimeoutMS=10000] How long to wait for a connection to be established before timing out
* @param {number} [options.socketTimeoutMS=360000] How long a send or receive on a socket can take before timing out
* @param {number} [options.family] Version of IP stack. Can be 4, 6 or null (default).
@@ -406,7 +407,7 @@ MongoClient.prototype.isConnected = function(options) {
* @param {string} [options.appname=undefined] The name of the application that created this MongoClient instance. MongoDB 3.4 and newer will print this value in the server log upon establishing each connection. It is also recorded in the slow query log and profile collections
* @param {string} [options.auth.user=undefined] The username for auth
* @param {string} [options.auth.password=undefined] The password for auth
* @param {string} [options.authMechanism=undefined] Mechanism for authentication: MDEFAULT, GSSAPI, PLAIN, MONGODB-X509, or SCRAM-SHA-1
* @param {string} [options.authMechanism] An authentication mechanism to use for connection authentication, see the {@link https://docs.mongodb.com/manual/reference/connection-string/#urioption.authMechanism|authMechanism} reference for supported options.
* @param {object} [options.compression] Type of compression to use: snappy or zlib
* @param {boolean} [options.fsync=false] Specify a file sync write concern
* @param {array} [options.readPreferenceTags] Read preference tags
@@ -414,6 +415,7 @@ MongoClient.prototype.isConnected = function(options) {
* @param {boolean} [options.auto_reconnect=true] Enable auto reconnecting for single server instances
* @param {boolean} [options.monitorCommands=false] Enable command monitoring for this client
* @param {number} [options.minSize] If present, the connection pool will be initialized with minSize connections, and will never dip below minSize connections
* @param {boolean} [options.directConnection=false] Enable directConnection
* @param {boolean} [options.useNewUrlParser=true] Determines whether or not to use the new url parser. Enables the new, spec-compliant, url parser shipped in the core driver. This url parser fixes a number of problems with the original parser, and aims to outright replace that parser in the near future. Defaults to true, and must be explicitly set to false to use the legacy url parser.
* @param {boolean} [options.useUnifiedTopology] Enables the new unified topology layer
* @param {Number} [options.localThresholdMS=15] **Only applies to the unified topology** The size of the latency window for selecting among multiple suitable servers

View File

@@ -8,13 +8,11 @@ const decorateWithReadConcern = require('../utils').decorateWithReadConcern;
const ensureIndexDb = require('./db_ops').ensureIndex;
const evaluate = require('./db_ops').evaluate;
const executeCommand = require('./db_ops').executeCommand;
const resolveReadPreference = require('../utils').resolveReadPreference;
const handleCallback = require('../utils').handleCallback;
const indexInformationDb = require('./db_ops').indexInformation;
const Long = require('../core').BSON.Long;
const MongoError = require('../core').MongoError;
const ReadPreference = require('../core').ReadPreference;
const toError = require('../utils').toError;
const insertDocuments = require('./common_functions').insertDocuments;
const updateDocuments = require('./common_functions').updateDocuments;
@@ -52,24 +50,6 @@ const updateDocuments = require('./common_functions').updateDocuments;
const groupFunction =
'function () {\nvar c = db[ns].find(condition);\nvar map = new Map();\nvar reduce_function = reduce;\n\nwhile (c.hasNext()) {\nvar obj = c.next();\nvar key = {};\n\nfor (var i = 0, len = keys.length; i < len; ++i) {\nvar k = keys[i];\nkey[k] = obj[k];\n}\n\nvar aggObj = map.get(key);\n\nif (aggObj == null) {\nvar newObj = Object.extend({}, key);\naggObj = Object.extend(newObj, initial);\nmap.put(key, aggObj);\n}\n\nreduce_function(obj, aggObj);\n}\n\nreturn { "result": map.values() };\n}';
// Check the update operation to ensure it has atomic operators.
function checkForAtomicOperators(update) {
if (Array.isArray(update)) {
return update.reduce((err, u) => err || checkForAtomicOperators(u), null);
}
const keys = Object.keys(update);
// same errors as the server would give for update doc lacking atomic operators
if (keys.length === 0) {
return toError('The update operation document must contain at least one atomic operator.');
}
if (keys[0][0] !== '$') {
return toError('the update operation document must contain atomic operators.');
}
}
/**
* Create an index on the db and collection.
*
@@ -188,7 +168,7 @@ function group(coll, keys, condition, initial, reduce, finalize, command, option
options = Object.assign({}, options);
// Ensure we have the right read preference inheritance
options.readPreference = resolveReadPreference(coll, options);
options.readPreference = ReadPreference.resolve(coll, options);
// Do we have a readConcern specified
decorateWithReadConcern(selector, coll, options);
@@ -361,7 +341,6 @@ function save(coll, doc, options, callback) {
}
module.exports = {
checkForAtomicOperators,
createIndex,
createIndexes,
ensureIndex,

View File

@@ -7,7 +7,6 @@ const debugOptions = require('../utils').debugOptions;
const handleCallback = require('../utils').handleCallback;
const MongoError = require('../core').MongoError;
const ReadPreference = require('../core').ReadPreference;
const resolveReadPreference = require('../utils').resolveReadPreference;
const MongoDBNamespace = require('../utils').MongoDBNamespace;
const debugFields = [
@@ -38,9 +37,9 @@ class CommandOperation extends OperationBase {
if (!this.hasAspect(Aspect.WRITE_OPERATION)) {
if (collection != null) {
this.options.readPreference = resolveReadPreference(collection, options);
this.options.readPreference = ReadPreference.resolve(collection, options);
} else {
this.options.readPreference = resolveReadPreference(db, options);
this.options.readPreference = ReadPreference.resolve(db, options);
}
} else {
if (collection != null) {

View File

@@ -2,12 +2,12 @@
const Aspect = require('./operation').Aspect;
const OperationBase = require('./operation').OperationBase;
const resolveReadPreference = require('../utils').resolveReadPreference;
const ReadPreference = require('../core').ReadPreference;
const ReadConcern = require('../read_concern');
const WriteConcern = require('../write_concern');
const maxWireVersion = require('../core/utils').maxWireVersion;
const commandSupportsReadConcern = require('../core/sessions').commandSupportsReadConcern;
const MongoError = require('../error').MongoError;
const MongoError = require('../core/error').MongoError;
const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;
@@ -16,9 +16,10 @@ class CommandOperationV2 extends OperationBase {
super(options);
this.ns = parent.s.namespace.withCollection('$cmd');
this.readPreference = resolveReadPreference(parent, this.options);
this.readConcern = resolveReadConcern(parent, this.options);
this.writeConcern = resolveWriteConcern(parent, this.options);
const propertyProvider = this.hasAspect(Aspect.NO_INHERIT_OPTIONS) ? undefined : parent;
this.readPreference = ReadPreference.resolve(propertyProvider, this.options);
this.readConcern = resolveReadConcern(propertyProvider, this.options);
this.writeConcern = resolveWriteConcern(propertyProvider, this.options);
this.explain = false;
if (operationOptions && typeof operationOptions.fullResponse === 'boolean') {
@@ -97,11 +98,11 @@ class CommandOperationV2 extends OperationBase {
}
function resolveWriteConcern(parent, options) {
return WriteConcern.fromOptions(options) || parent.writeConcern;
return WriteConcern.fromOptions(options) || (parent && parent.writeConcern);
}
function resolveReadConcern(parent, options) {
return ReadConcern.fromOptions(options) || parent.readConcern;
return ReadConcern.fromOptions(options) || (parent && parent.readConcern);
}
module.exports = CommandOperationV2;

View File

@@ -297,6 +297,9 @@ function removeDocuments(coll, selector, options, callback) {
} else if (finalOptions.retryWrites) {
finalOptions.retryWrites = false;
}
if (options.hint) {
op.hint = options.hint;
}
// Have we specified collation
try {

View File

@@ -33,9 +33,11 @@ const legacyParse = deprecate(
const AUTH_MECHANISM_INTERNAL_MAP = {
DEFAULT: 'default',
'MONGODB-CR': 'mongocr',
PLAIN: 'plain',
GSSAPI: 'gssapi',
'MONGODB-CR': 'mongocr',
'MONGODB-X509': 'x509',
'MONGODB-AWS': 'mongodb-aws',
'SCRAM-SHA-1': 'scram-sha-1',
'SCRAM-SHA-256': 'scram-sha-256'
};
@@ -66,12 +68,13 @@ const monitoringEvents = [
const VALID_AUTH_MECHANISMS = new Set([
'DEFAULT',
'MONGODB-CR',
'PLAIN',
'GSSAPI',
'MONGODB-CR',
'MONGODB-X509',
'MONGODB-AWS',
'SCRAM-SHA-1',
'SCRAM-SHA-256',
'GSSAPI'
'SCRAM-SHA-256'
]);
const validOptionNames = [
@@ -151,6 +154,8 @@ const validOptionNames = [
'tlsCertificateKeyFilePassword',
'minHeartbeatFrequencyMS',
'heartbeatFrequencyMS',
'directConnection',
'appName',
// CMAP options
'maxPoolSize',
@@ -644,6 +649,7 @@ function generateCredentials(client, username, password, options) {
// authMechanism
const authMechanismRaw = options.authMechanism || 'DEFAULT';
const authMechanism = authMechanismRaw.toUpperCase();
const mechanismProperties = options.authMechanismProperties;
if (!VALID_AUTH_MECHANISMS.has(authMechanism)) {
throw MongoError.create({
@@ -652,18 +658,9 @@ function generateCredentials(client, username, password, options) {
});
}
if (authMechanism === 'GSSAPI') {
return new MongoCredentials({
mechanism: process.platform === 'win32' ? 'sspi' : 'gssapi',
mechanismProperties: options,
source,
username,
password
});
}
return new MongoCredentials({
mechanism: AUTH_MECHANISM_INTERNAL_MAP[authMechanism],
mechanismProperties,
source,
username,
password

View File

@@ -4,13 +4,11 @@ const Aspect = require('./operation').Aspect;
const defineAspects = require('./operation').defineAspects;
const CommandOperation = require('./command');
const applyWriteConcern = require('../utils').applyWriteConcern;
const handleCallback = require('../utils').handleCallback;
const loadCollection = require('../dynamic_loaders').loadCollection;
const MongoError = require('../core').MongoError;
const ReadPreference = require('../core').ReadPreference;
// Filter out any write concern options
const illegalCommandFields = [
const ILLEGAL_COMMAND_FIELDS = new Set([
'w',
'wtimeout',
'j',
@@ -24,12 +22,11 @@ const illegalCommandFields = [
'session',
'readConcern',
'writeConcern'
];
]);
class CreateCollectionOperation extends CommandOperation {
constructor(db, name, options) {
super(db, options);
this.name = name;
}
@@ -37,14 +34,12 @@ class CreateCollectionOperation extends CommandOperation {
const name = this.name;
const options = this.options;
// Create collection command
const cmd = { create: name };
// Add all optional parameters
for (let n in options) {
if (
options[n] != null &&
typeof options[n] !== 'function' &&
illegalCommandFields.indexOf(n) === -1
!ILLEGAL_COMMAND_FIELDS.has(n)
) {
cmd[n] = options[n];
}
@@ -57,61 +52,51 @@ class CreateCollectionOperation extends CommandOperation {
const db = this.db;
const name = this.name;
const options = this.options;
const Collection = loadCollection();
let Collection = loadCollection();
// Did the user destroy the topology
if (db.serverConfig && db.serverConfig.isDestroyed()) {
return callback(new MongoError('topology was destroyed'));
}
let listCollectionOptions = Object.assign({}, options, { nameOnly: true });
let listCollectionOptions = Object.assign({ nameOnly: true, strict: false }, options);
listCollectionOptions = applyWriteConcern(listCollectionOptions, { db }, listCollectionOptions);
// Check if we have the name
db.listCollections({ name }, listCollectionOptions)
.setReadPreference(ReadPreference.PRIMARY)
.toArray((err, collections) => {
if (err != null) return handleCallback(callback, err, null);
if (collections.length > 0 && listCollectionOptions.strict) {
return handleCallback(
callback,
MongoError.create({
message: `Collection ${name} already exists. Currently in strict mode.`,
driver: true
}),
null
);
} else if (collections.length > 0) {
try {
return handleCallback(
callback,
null,
new Collection(db, db.s.topology, db.databaseName, name, db.s.pkFactory, options)
);
} catch (err) {
return handleCallback(callback, err);
}
}
function done(err) {
if (err) {
return callback(err);
}
// Execute command
super.execute(err => {
if (err) return handleCallback(callback, err);
try {
callback(
null,
new Collection(db, db.s.topology, db.databaseName, name, db.s.pkFactory, options)
);
} catch (err) {
callback(err);
}
}
try {
return handleCallback(
callback,
null,
new Collection(db, db.s.topology, db.databaseName, name, db.s.pkFactory, options)
);
} catch (err) {
return handleCallback(callback, err);
const strictMode = listCollectionOptions.strict;
if (strictMode) {
db.listCollections({ name }, listCollectionOptions)
.setReadPreference(ReadPreference.PRIMARY)
.toArray((err, collections) => {
if (err) {
return callback(err);
}
if (collections.length > 0) {
return callback(
new MongoError(`Collection ${name} already exists. Currently in strict mode.`)
);
}
super.execute(done);
});
});
return;
}
// otherwise just execute the command
super.execute(done);
}
}
defineAspects(CreateCollectionOperation, Aspect.WRITE_OPERATION);
module.exports = CreateCollectionOperation;

View File

@@ -2,60 +2,116 @@
const Aspect = require('./operation').Aspect;
const defineAspects = require('./operation').defineAspects;
const OperationBase = require('./operation').OperationBase;
const executeCommand = require('./db_ops').executeCommand;
const CommandOperationV2 = require('./command_v2');
const MongoError = require('../core').MongoError;
const ReadPreference = require('../core').ReadPreference;
const parseIndexOptions = require('../utils').parseIndexOptions;
const maxWireVersion = require('../core/utils').maxWireVersion;
class CreateIndexesOperation extends OperationBase {
constructor(collection, indexSpecs, options) {
super(options);
const validIndexOptions = new Set([
'unique',
'partialFilterExpression',
'sparse',
'background',
'expireAfterSeconds',
'storageEngine',
'collation',
'bucketSize'
]);
class CreateIndexesOperation extends CommandOperationV2 {
/**
* @ignore
*/
constructor(parent, collection, indexes, options) {
super(parent, options);
this.collection = collection;
this.indexSpecs = indexSpecs;
// createIndex can be called with a variety of styles:
// coll.createIndex('a');
// coll.createIndex({ a: 1 });
// coll.createIndex([['a', 1]]);
// createIndexes is always called with an array of index spec objects
if (!Array.isArray(indexes) || Array.isArray(indexes[0])) {
this.onlyReturnNameOfCreatedIndex = true;
// TODO: remove in v4 (breaking change); make createIndex return full response as createIndexes does
const indexParameters = parseIndexOptions(indexes);
// Generate the index name
const name = typeof options.name === 'string' ? options.name : indexParameters.name;
// Set up the index
const indexSpec = { name, key: indexParameters.fieldHash };
// merge valid index options into the index spec
for (let optionName in options) {
if (validIndexOptions.has(optionName)) {
indexSpec[optionName] = options[optionName];
}
}
this.indexes = [indexSpec];
return;
}
this.indexes = indexes;
}
execute(callback) {
const coll = this.collection;
const indexSpecs = this.indexSpecs;
let options = this.options;
/**
* @ignore
*/
execute(server, callback) {
const options = this.options;
const indexes = this.indexes;
const capabilities = coll.s.topology.capabilities();
const serverWireVersion = maxWireVersion(server);
// Ensure we generate the correct name if the parameter is not set
for (let i = 0; i < indexSpecs.length; i++) {
if (indexSpecs[i].name == null) {
for (let i = 0; i < indexes.length; i++) {
// Did the user pass in a collation, check if our write server supports it
if (indexes[i].collation && serverWireVersion < 5) {
callback(
new MongoError(
`Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
)
);
return;
}
if (indexes[i].name == null) {
const keys = [];
// Did the user pass in a collation, check if our write server supports it
if (indexSpecs[i].collation && capabilities && !capabilities.commandsTakeCollation) {
return callback(new MongoError('server/primary/mongos does not support collation'));
}
for (let name in indexSpecs[i].key) {
keys.push(`${name}_${indexSpecs[i].key[name]}`);
for (let name in indexes[i].key) {
keys.push(`${name}_${indexes[i].key[name]}`);
}
// Set the name
indexSpecs[i].name = keys.join('_');
indexes[i].name = keys.join('_');
}
}
options = Object.assign({}, options, { readPreference: ReadPreference.PRIMARY });
const cmd = { createIndexes: this.collection, indexes };
// Execute the index
executeCommand(
coll.s.db,
{
createIndexes: coll.collectionName,
indexes: indexSpecs
},
options,
callback
);
if (options.commitQuorum != null) {
if (serverWireVersion < 9) {
callback(
new MongoError('`commitQuorum` option for `createIndexes` not supported on servers < 4.4')
);
return;
}
cmd.commitQuorum = options.commitQuorum;
}
// collation is set on each index, it should not be defined at the root
this.options.collation = undefined;
super.executeCommand(server, cmd, (err, result) => {
if (err) {
callback(err);
return;
}
callback(null, this.onlyReturnNameOfCreatedIndex ? indexes[0].name : result);
});
}
}
defineAspects(CreateIndexesOperation, Aspect.WRITE_OPERATION);
defineAspects(CreateIndexesOperation, [Aspect.WRITE_OPERATION, Aspect.EXECUTE_WITH_SELECTION]);
module.exports = CreateIndexesOperation;

View File

@@ -2,7 +2,6 @@
const applyWriteConcern = require('../utils').applyWriteConcern;
const Code = require('../core').BSON.Code;
const resolveReadPreference = require('../utils').resolveReadPreference;
const debugOptions = require('../utils').debugOptions;
const handleCallback = require('../utils').handleCallback;
const MongoError = require('../core').MongoError;
@@ -225,7 +224,7 @@ function executeCommand(db, command, options, callback) {
const dbName = options.dbName || options.authdb || db.databaseName;
// Convert the readPreference if its not a write
options.readPreference = resolveReadPreference(db, options);
options.readPreference = ReadPreference.resolve(db, options);
// Debug information
if (db.s.logger.isDebug())

View File

@@ -3,7 +3,9 @@
const OperationBase = require('./operation').OperationBase;
const Aspect = require('./operation').Aspect;
const defineAspects = require('./operation').defineAspects;
const resolveReadPreference = require('../utils').resolveReadPreference;
const ReadPreference = require('../core').ReadPreference;
const maxWireVersion = require('../core/utils').maxWireVersion;
const MongoError = require('../core/error').MongoError;
class FindOperation extends OperationBase {
constructor(collection, ns, command, options) {
@@ -11,16 +13,20 @@ class FindOperation extends OperationBase {
this.ns = ns;
this.cmd = command;
this.readPreference = resolveReadPreference(collection, this.options);
this.readPreference = ReadPreference.resolve(collection, this.options);
}
execute(server, callback) {
// copied from `CommandOperationV2`, to be subclassed in the future
this.server = server;
const cursorState = this.cursorState || {};
if (typeof this.cmd.allowDiskUse !== 'undefined' && maxWireVersion(server) < 4) {
callback(new MongoError('The `allowDiskUse` option is not supported on MongoDB < 3.2'));
return;
}
// TOOD: use `MongoDBNamespace` through and through
const cursorState = this.cursorState || {};
server.query(this.ns.toString(), this.cmd, cursorState, this.options, callback);
}
}

View File

@@ -8,6 +8,8 @@ const executeCommand = require('./db_ops').executeCommand;
const formattedOrderClause = require('../utils').formattedOrderClause;
const handleCallback = require('../utils').handleCallback;
const ReadPreference = require('../core').ReadPreference;
const maxWireVersion = require('../core/utils').maxWireVersion;
const MongoError = require('../error').MongoError;
class FindAndModifyOperation extends OperationBase {
constructor(collection, query, sort, doc, options) {
@@ -86,6 +88,21 @@ class FindAndModifyOperation extends OperationBase {
return callback(err, null);
}
if (options.hint) {
// TODO: once this method becomes a CommandOperationV2 we will have the server
// in place to check.
const unacknowledgedWrite = options.writeConcern && options.writeConcern.w === 0;
if (unacknowledgedWrite || maxWireVersion(coll.s.topology) < 8) {
callback(
new MongoError('The current topology does not support a hint on findAndModify commands')
);
return;
}
queryObject.hint = options.hint;
}
// Execute the command
executeCommand(coll.s.db, queryObject, options, (err, result) => {
if (err) return handleCallback(callback, err, null);

View File

@@ -9,6 +9,11 @@ class FindOneAndDeleteOperation extends FindAndModifyOperation {
finalOptions.fields = options.projection;
finalOptions.remove = true;
// Basic validation
if (filter == null || typeof filter !== 'object') {
throw new TypeError('Filter parameter must be an object');
}
super(collection, filter, finalOptions.sort, null, finalOptions);
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const FindAndModifyOperation = require('./find_and_modify');
const hasAtomicOperators = require('../utils').hasAtomicOperators;
class FindOneAndReplaceOperation extends FindAndModifyOperation {
constructor(collection, filter, replacement, options) {
@@ -11,6 +12,18 @@ class FindOneAndReplaceOperation extends FindAndModifyOperation {
finalOptions.new = options.returnOriginal !== void 0 ? !options.returnOriginal : false;
finalOptions.upsert = options.upsert !== void 0 ? !!options.upsert : false;
if (filter == null || typeof filter !== 'object') {
throw new TypeError('Filter parameter must be an object');
}
if (replacement == null || typeof replacement !== 'object') {
throw new TypeError('Replacement parameter must be an object');
}
if (hasAtomicOperators(replacement)) {
throw new TypeError('Replacement document must not contain atomic operators');
}
super(collection, filter, finalOptions.sort, replacement, finalOptions);
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const FindAndModifyOperation = require('./find_and_modify');
const hasAtomicOperators = require('../utils').hasAtomicOperators;
class FindOneAndUpdateOperation extends FindAndModifyOperation {
constructor(collection, filter, update, options) {
@@ -12,6 +13,18 @@ class FindOneAndUpdateOperation extends FindAndModifyOperation {
typeof options.returnOriginal === 'boolean' ? !options.returnOriginal : false;
finalOptions.upsert = typeof options.upsert === 'boolean' ? options.upsert : false;
if (filter == null || typeof filter !== 'object') {
throw new TypeError('Filter parameter must be an object');
}
if (update == null || typeof update !== 'object') {
throw new TypeError('Update parameter must be an object');
}
if (!hasAtomicOperators(update)) {
throw new TypeError('Update document requires atomic operators');
}
super(collection, filter, finalOptions.sort, update, finalOptions);
}
}

View File

@@ -7,7 +7,7 @@ const decorateCommand = require('../utils').decorateCommand;
const decorateWithReadConcern = require('../utils').decorateWithReadConcern;
const executeCommand = require('./db_ops').executeCommand;
const handleCallback = require('../utils').handleCallback;
const resolveReadPreference = require('../utils').resolveReadPreference;
const ReadPreference = require('../core').ReadPreference;
const toError = require('../utils').toError;
/**
@@ -58,7 +58,7 @@ class GeoHaystackSearchOperation extends OperationBase {
options = Object.assign({}, options);
// Ensure we have the right read preference inheritance
options.readPreference = resolveReadPreference(coll, options);
options.readPreference = ReadPreference.resolve(coll, options);
// Do we have a readConcern specified
decorateWithReadConcern(commandObject, coll, options);

View File

@@ -9,7 +9,7 @@ const handleCallback = require('../utils').handleCallback;
const isObject = require('../utils').isObject;
const loadDb = require('../dynamic_loaders').loadDb;
const OperationBase = require('./operation').OperationBase;
const resolveReadPreference = require('../utils').resolveReadPreference;
const ReadPreference = require('../core').ReadPreference;
const toError = require('../utils').toError;
const exclusionList = [
@@ -60,7 +60,7 @@ class MapReduceOperation extends OperationBase {
let options = this.options;
const mapCommandHash = {
mapreduce: coll.collectionName,
mapReduce: coll.collectionName,
map: map,
reduce: reduce
};
@@ -80,7 +80,7 @@ class MapReduceOperation extends OperationBase {
options = Object.assign({}, options);
// Ensure we have the right read preference inheritance
options.readPreference = resolveReadPreference(coll, options);
options.readPreference = ReadPreference.resolve(coll, options);
// If we have a read preference and inline is not set as output fail hard
if (

View File

@@ -4,7 +4,8 @@ const Aspect = {
READ_OPERATION: Symbol('READ_OPERATION'),
WRITE_OPERATION: Symbol('WRITE_OPERATION'),
RETRYABLE: Symbol('RETRYABLE'),
EXECUTE_WITH_SELECTION: Symbol('EXECUTE_WITH_SELECTION')
EXECUTE_WITH_SELECTION: Symbol('EXECUTE_WITH_SELECTION'),
NO_INHERIT_OPTIONS: Symbol('NO_INHERIT_OPTIONS')
};
/**

View File

@@ -1,28 +1,33 @@
'use strict';
const CommandOperation = require('./command');
const handleCallback = require('../utils').handleCallback;
const Aspect = require('./operation').Aspect;
const defineAspects = require('./operation').defineAspects;
const CommandOperationV2 = require('./command_v2');
const serverType = require('../core/sdam/common').serverType;
const ServerType = require('../core/sdam/common').ServerType;
const MongoError = require('../core').MongoError;
class ReIndexOperation extends CommandOperation {
class ReIndexOperation extends CommandOperationV2 {
constructor(collection, options) {
super(collection.s.db, options, collection);
super(collection, options);
this.collectionName = collection.collectionName;
}
_buildCommand() {
const collection = this.collection;
const cmd = { reIndex: collection.collectionName };
return cmd;
}
execute(callback) {
super.execute((err, result) => {
if (callback == null) return;
if (err) return handleCallback(callback, err, null);
handleCallback(callback, null, result.ok ? true : false);
execute(server, callback) {
if (serverType(server) !== ServerType.Standalone) {
callback(new MongoError(`reIndex can only be executed on standalone servers.`));
return;
}
super.executeCommand(server, { reIndex: this.collectionName }, (err, result) => {
if (err) {
callback(err);
return;
}
callback(null, !!result.ok);
});
}
}
defineAspects(ReIndexOperation, [Aspect.EXECUTE_WITH_SELECTION]);
module.exports = ReIndexOperation;

View File

@@ -2,27 +2,34 @@
const OperationBase = require('./operation').OperationBase;
const updateDocuments = require('./common_functions').updateDocuments;
const hasAtomicOperators = require('../utils').hasAtomicOperators;
class ReplaceOneOperation extends OperationBase {
constructor(collection, filter, doc, options) {
constructor(collection, filter, replacement, options) {
super(options);
if (hasAtomicOperators(replacement)) {
throw new TypeError('Replacement document must not contain atomic operators');
}
this.collection = collection;
this.filter = filter;
this.doc = doc;
this.replacement = replacement;
}
execute(callback) {
const coll = this.collection;
const filter = this.filter;
const doc = this.doc;
const replacement = this.replacement;
const options = this.options;
// Set single document update
options.multi = false;
// Execute update
updateDocuments(coll, filter, doc, options, (err, r) => replaceCallback(err, r, doc, callback));
updateDocuments(coll, filter, replacement, options, (err, r) =>
replaceCallback(err, r, replacement, callback)
);
}
}

View File

@@ -3,11 +3,16 @@
const OperationBase = require('./operation').OperationBase;
const updateCallback = require('./common_functions').updateCallback;
const updateDocuments = require('./common_functions').updateDocuments;
const hasAtomicOperators = require('../utils').hasAtomicOperators;
class UpdateManyOperation extends OperationBase {
constructor(collection, filter, update, options) {
super(options);
if (!hasAtomicOperators(update)) {
throw new TypeError('Update document requires atomic operators');
}
this.collection = collection;
this.filter = filter;
this.update = update;

View File

@@ -2,11 +2,16 @@
const OperationBase = require('./operation').OperationBase;
const updateDocuments = require('./common_functions').updateDocuments;
const hasAtomicOperators = require('../utils').hasAtomicOperators;
class UpdateOneOperation extends OperationBase {
constructor(collection, filter, update, options) {
super(options);
if (!hasAtomicOperators(update)) {
throw new TypeError('Update document requires atomic operators');
}
this.collection = collection;
this.filter = filter;
this.update = update;

View File

@@ -14,8 +14,7 @@ class ValidateCollectionOperation extends CommandOperation {
}
super(admin.s.db, options, null, command);
this.collectionName;
this.collectionName = collectionName;
}
execute(callback) {

View File

@@ -82,7 +82,7 @@ var legalOptionNames = [
* @param {object} [options.socketOptions] Socket options
* @param {boolean} [options.socketOptions.noDelay=true] TCP Socket NoDelay option.
* @param {boolean} [options.socketOptions.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.socketOptions.keepAliveInitialDelay=30000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.keepAliveInitialDelay=120000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.connectTimeoutMS=10000] How long to wait for a connection to be established before timing out
* @param {number} [options.socketOptions.socketTimeoutMS=360000] How long a send or receive on a socket can take before timing out
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.

View File

@@ -92,7 +92,7 @@ var legalOptionNames = [
* @param {object} [options.socketOptions] Socket options
* @param {boolean} [options.socketOptions.noDelay=true] TCP Socket NoDelay option.
* @param {boolean} [options.socketOptions.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.socketOptions.keepAliveInitialDelay=30000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.keepAliveInitialDelay=120000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.connectTimeoutMS=10000] How long to wait for a connection to be established before timing out
* @param {number} [options.socketOptions.socketTimeoutMS=360000] How long a send or receive on a socket can take before timing out
* @param {boolean} [options.domainsEnabled=false] Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit.

View File

@@ -84,7 +84,7 @@ var legalOptionNames = [
* @param {boolean} [options.socketOptions.autoReconnect=true] Reconnect on error.
* @param {boolean} [options.socketOptions.noDelay=true] TCP Socket NoDelay option.
* @param {boolean} [options.socketOptions.keepAlive=true] TCP Connection keep alive enabled
* @param {number} [options.socketOptions.keepAliveInitialDelay=30000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.keepAliveInitialDelay=120000] The number of milliseconds to wait before initiating keepAlive on the TCP socket
* @param {number} [options.socketOptions.connectTimeoutMS=10000] How long to wait for a connection to be established before timing out
* @param {number} [options.socketOptions.socketTimeoutMS=360000] How long a send or receive on a socket can take before timing out
* @param {number} [options.reconnectTries=30] Server attempt to reconnect #times

View File

@@ -3,7 +3,7 @@
const EventEmitter = require('events'),
MongoError = require('../core').MongoError,
f = require('util').format,
translateReadPreference = require('../utils').translateReadPreference,
ReadPreference = require('../core').ReadPreference,
ClientSession = require('../core').Sessions.ClientSession;
// The store of ops
@@ -293,7 +293,7 @@ class TopologyBase extends EventEmitter {
// Command
command(ns, cmd, options, callback) {
this.s.coreTopology.command(ns.toString(), cmd, translateReadPreference(options), callback);
this.s.coreTopology.command(ns.toString(), cmd, ReadPreference.translate(options), callback);
}
// Insert
@@ -314,7 +314,7 @@ class TopologyBase extends EventEmitter {
// IsConnected
isConnected(options) {
options = options || {};
options = translateReadPreference(options);
options = ReadPreference.translate(options);
return this.s.coreTopology.isConnected(options);
}
@@ -327,7 +327,7 @@ class TopologyBase extends EventEmitter {
// Cursor
cursor(ns, cmd, options) {
options = options || {};
options = translateReadPreference(options);
options = ReadPreference.translate(options);
options.disconnectHandler = this.s.store;
options.topology = this;

78
node_modules/mongodb/lib/utils.js generated vendored
View File

@@ -1,6 +1,5 @@
'use strict';
const MongoError = require('./core/error').MongoError;
const ReadPreference = require('./core/topologies/read_preference');
const WriteConcern = require('./write_concern');
var shallowClone = function(obj) {
@@ -9,31 +8,6 @@ var shallowClone = function(obj) {
return copy;
};
// Figure out the read preference
var translateReadPreference = function(options) {
var r = null;
if (options.readPreference) {
r = options.readPreference;
} else {
return options;
}
if (typeof r === 'string') {
options.readPreference = new ReadPreference(r);
} else if (r && !(r instanceof ReadPreference) && typeof r === 'object') {
const mode = r.mode || r.preference;
if (mode && typeof mode === 'string') {
options.readPreference = new ReadPreference(mode, r.tags, {
maxStalenessSeconds: r.maxStalenessSeconds
});
}
} else if (!(r instanceof ReadPreference)) {
throw new TypeError('Invalid read preference: ' + r);
}
return options;
};
// Set simple property
var getSingleProperty = function(obj, name, value) {
Object.defineProperty(obj, name, {
@@ -490,37 +464,6 @@ function applyWriteConcern(target, sources, options) {
return target;
}
/**
* Resolves a read preference based on well-defined inheritance rules. This method will not only
* determine the read preference (if there is one), but will also ensure the returned value is a
* properly constructed instance of `ReadPreference`.
*
* @param {Collection|Db|MongoClient} parent The parent of the operation on which to determine the read
* preference, used for determining the inherited read preference.
* @param {Object} options The options passed into the method, potentially containing a read preference
* @returns {(ReadPreference|null)} The resolved read preference
*/
function resolveReadPreference(parent, options) {
options = options || {};
const session = options.session;
const inheritedReadPreference = parent.readPreference;
let readPreference;
if (options.readPreference) {
readPreference = ReadPreference.fromOptions(options);
} else if (session && session.inTransaction() && session.transaction.options.readPreference) {
// The transactions read preference MUST override all other user configurable read preferences.
readPreference = session.transaction.options.readPreference;
} else if (inheritedReadPreference != null) {
readPreference = inheritedReadPreference;
} else {
throw new Error('No readPreference was provided or inherited.');
}
return typeof readPreference === 'string' ? new ReadPreference(readPreference) : readPreference;
}
/**
* Checks if a given value is a Promise
*
@@ -778,6 +721,12 @@ function makeInterruptableAsyncInterval(fn, options) {
const timeUntilNextCall = Math.max(interval - timeSinceLastCall, 0);
lastWakeTime = currentTime;
// For the streaming protocol: there is nothing obviously stopping this
// interval from being woken up again while we are waiting "infinitely"
// for `fn` to be called again`. Since the function effectively
// never completes, the `timeUntilNextCall` will continue to grow
// negatively unbounded, so it will never trigger a reschedule here.
// debounce multiple calls to wake within the `minInterval`
if (timeSinceLastWake < minInterval) {
return;
@@ -810,6 +759,7 @@ function makeInterruptableAsyncInterval(fn, options) {
function executeAndReschedule() {
lastWakeTime = 0;
lastCallTime = now();
fn(err => {
if (err) throw err;
reschedule(interval);
@@ -826,6 +776,15 @@ function makeInterruptableAsyncInterval(fn, options) {
return { wake, stop };
}
function hasAtomicOperators(doc) {
if (Array.isArray(doc)) {
return doc.reduce((err, u) => err || hasAtomicOperators(u), null);
}
const keys = Object.keys(doc);
return keys.length > 0 && keys[0][0] === '$';
}
module.exports = {
filterOptions,
mergeOptions,
@@ -843,7 +802,6 @@ module.exports = {
debugOptions,
MAX_JS_INT: Number.MAX_SAFE_INTEGER + 1,
mergeOptionsAndWriteConcern,
translateReadPreference,
executeLegacyOperation,
applyRetryableWrites,
applyWriteConcern,
@@ -853,11 +811,11 @@ module.exports = {
deprecateOptions,
SUPPORTS,
MongoDBNamespace,
resolveReadPreference,
emitDeprecationWarning,
makeCounter,
maybePromise,
now,
calculateDurationInMs,
makeInterruptableAsyncInterval
makeInterruptableAsyncInterval,
hasAtomicOperators
};

View File

@@ -1,5 +1,7 @@
'use strict';
const kWriteConcernKeys = new Set(['w', 'wtimeout', 'j', 'fsync']);
/**
* The **WriteConcern** class is a class that represents a MongoDB WriteConcern.
* @class
@@ -51,6 +53,14 @@ class WriteConcern {
}
if (options.writeConcern) {
if (typeof options.writeConcern === 'string') {
return new WriteConcern(options.writeConcern);
}
if (!Object.keys(options.writeConcern).some(key => kWriteConcernKeys.has(key))) {
return;
}
return new WriteConcern(
options.writeConcern.w,
options.writeConcern.wtimeout,

102
node_modules/mongodb/package.json generated vendored
View File

@@ -1,41 +1,35 @@
{
"_from": "mongodb@3.5.9",
"_id": "mongodb@3.5.9",
"_inBundle": false,
"_integrity": "sha512-vXHBY1CsGYcEPoVWhwgxIBeWqP3dSu9RuRDsoLRPTITrcrgm1f0Ubu1xqF9ozMwv53agmEiZm0YGo+7WL3Nbug==",
"_location": "/mongodb",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "mongodb@3.5.9",
"name": "mongodb",
"escapedName": "mongodb",
"rawSpec": "3.5.9",
"saveSpec": null,
"fetchSpec": "3.5.9"
},
"_requiredBy": [
"/mongoose"
"name": "mongodb",
"version": "3.6.0",
"description": "The official MongoDB driver for Node.js",
"main": "index.js",
"files": [
"index.js",
"lib"
],
"_resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.9.tgz",
"_shasum": "799b72be8110b7e71a882bb7ce0d84d05429f772",
"_spec": "mongodb@3.5.9",
"_where": "D:\\WORK\\Menui\\menui_backend\\node_modules\\mongoose",
"bugs": {
"url": "https://github.com/mongodb/node-mongodb-native/issues"
"repository": {
"type": "git",
"url": "git@github.com:mongodb/node-mongodb-native.git"
},
"keywords": [
"mongodb",
"driver",
"official"
],
"peerOptionalDependencies": {
"kerberos": "^1.1.0",
"mongodb-client-encryption": "^1.0.0",
"mongodb-extjson": "^2.1.2",
"snappy": "^6.3.4",
"bson-ext": "^2.0.0"
},
"bundleDependencies": false,
"dependencies": {
"bl": "^2.2.0",
"bson": "^1.1.4",
"denque": "^1.4.1",
"require_optional": "^1.0.1",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
"safe-buffer": "^5.1.2"
},
"deprecated": false,
"description": "The official MongoDB driver for Node.js",
"devDependencies": {
"chai": "^4.1.1",
"chai-subset": "^1.6.0",
@@ -62,46 +56,26 @@
"wtfnode": "^0.8.0",
"yargs": "^14.2.0"
},
"license": "Apache-2.0",
"engines": {
"node": ">=4"
},
"files": [
"index.js",
"lib"
],
"homepage": "https://github.com/mongodb/node-mongodb-native",
"keywords": [
"mongodb",
"driver",
"official"
],
"license": "Apache-2.0",
"main": "index.js",
"name": "mongodb",
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerOptionalDependencies": {
"kerberos": "^1.1.0",
"mongodb-client-encryption": "^1.0.0",
"mongodb-extjson": "^2.1.2",
"snappy": "^6.3.4",
"bson-ext": "^2.0.0"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/mongodb/node-mongodb-native.git"
"bugs": {
"url": "https://github.com/mongodb/node-mongodb-native/issues"
},
"scripts": {
"atlas": "node ./test/tools/atlas_connectivity_tests.js",
"bench": "node test/benchmarks/driverBench/",
"coverage": "istanbul cover mongodb-test-runner -- -t 60000 test/core test/unit test/functional",
"format": "prettier --print-width 100 --tab-width 2 --single-quote --write 'test/**/*.js' 'lib/**/*.js'",
"generate-evergreen": "node .evergreen/generate_evergreen_tasks.js",
"lint": "eslint -v && eslint lib test",
"release": "standard-version -i HISTORY.md",
"atlas": "mocha --opts '{}' ./test/manual/atlas_connectivity.test.js",
"test": "npm run lint && mocha --recursive test/functional test/unit test/core",
"test-nolint": "mocha --recursive test/functional test/unit test/core"
"test-nolint": "mocha --recursive test/functional test/unit test/core",
"coverage": "istanbul cover mongodb-test-runner -- -t 60000 test/core test/unit test/functional",
"lint": "eslint -v && eslint lib test",
"format": "prettier --print-width 100 --tab-width 2 --single-quote --write 'test/**/*.js' 'lib/**/*.js'",
"bench": "node test/benchmarks/driverBench/",
"generate-evergreen": "node .evergreen/generate_evergreen_tasks.js",
"release": "standard-version -i HISTORY.md"
},
"version": "3.5.9"
"homepage": "https://github.com/mongodb/node-mongodb-native",
"optionalDependencies": {
"saslprep": "^1.0.0"
}
}