mirror of
https://github.com/bvanroll/yahoo-thing.git
synced 2025-08-29 20:12:46 +00:00
euh
This commit is contained in:
29
node_modules/mongodb-core/lib/auth/defaultAuthProviders.js
generated
vendored
Normal file
29
node_modules/mongodb-core/lib/auth/defaultAuthProviders.js
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Returns the default authentication providers.
|
||||
*
|
||||
* @param {BSON} bson Bson definition
|
||||
* @returns {Object} a mapping of auth names to auth types
|
||||
*/
|
||||
function defaultAuthProviders(bson) {
|
||||
return {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { defaultAuthProviders };
|
381
node_modules/mongodb-core/lib/auth/gssapi.js
generated
vendored
Normal file
381
node_modules/mongodb-core/lib/auth/gssapi.js
generated
vendored
Normal file
@@ -0,0 +1,381 @@
|
||||
'use strict';
|
||||
|
||||
const f = require('util').format;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const retrieveKerberos = require('../utils').retrieveKerberos;
|
||||
|
||||
var AuthSession = function(db, username, password, options) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new GSSAPI authentication mechanism
|
||||
* @class
|
||||
* @return {GSSAPI} A cursor instance
|
||||
*/
|
||||
var GSSAPI = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.auth = function(server, connections, db, username, password, options, callback) {
|
||||
var self = this;
|
||||
let kerberos;
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
// TODO: remove this once we fix URI parsing
|
||||
var gssapiServiceName = options['gssapiservicename'] || options['gssapiServiceName'] || 'mongodb';
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Start Auth process for a connection
|
||||
GSSAPIInitialize(
|
||||
self,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
db,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
connection,
|
||||
options,
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password, options));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Initialize step
|
||||
var GSSAPIInitialize = function(
|
||||
self,
|
||||
MongoAuthProcess,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
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,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Perform first step against mongodb
|
||||
var MongoDBGSSAPIFirstStep = function(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
) {
|
||||
// Build the sasl start command
|
||||
var command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'GSSAPI',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (err) return callback(err, false);
|
||||
var doc = r.result;
|
||||
// Execute mongodb transition
|
||||
mongo_auth_process.transition(r.result.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,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Perform first step against mongodb
|
||||
var MongoDBGSSAPISecondStep = function(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
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
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (err) return callback(err, false);
|
||||
var doc = r.result;
|
||||
// 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,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var MongoDBGSSAPIThirdStep = function(
|
||||
self,
|
||||
mongo_auth_process,
|
||||
payload,
|
||||
doc,
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
authdb,
|
||||
server,
|
||||
connection,
|
||||
callback
|
||||
) {
|
||||
// Build final command
|
||||
var command = {
|
||||
saslContinue: 1,
|
||||
conversationId: doc.conversationId,
|
||||
payload: payload
|
||||
};
|
||||
|
||||
// Execute the command
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (err) return callback(err, false);
|
||||
mongo_auth_process.transition(null, function(err) {
|
||||
if (err) return callback(err, null);
|
||||
callback(null, r);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
GSSAPI.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
authStore[i].options,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
214
node_modules/mongodb-core/lib/auth/mongocr.js
generated
vendored
Normal file
214
node_modules/mongodb-core/lib/auth/mongocr.js
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
crypto = require('crypto'),
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new MongoCR authentication mechanism
|
||||
* @class
|
||||
* @return {MongoCR} A cursor instance
|
||||
*/
|
||||
var MongoCR = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var executeMongoCR = function(connection) {
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(
|
||||
self.bson,
|
||||
f('%s.$cmd', db),
|
||||
{
|
||||
getnonce: 1
|
||||
},
|
||||
{
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}
|
||||
),
|
||||
function(err, r) {
|
||||
var nonce = null;
|
||||
var key = null;
|
||||
|
||||
// Adjust the number of connections left
|
||||
// Get nonce
|
||||
if (err == null) {
|
||||
nonce = r.result.nonce;
|
||||
// Use node md5 generator
|
||||
var md5 = crypto.createHash('md5');
|
||||
// Generate keys used for authentication
|
||||
md5.update(username + ':mongo:' + password, 'utf8');
|
||||
var hash_password = md5.digest('hex');
|
||||
// Final key
|
||||
md5 = crypto.createHash('md5');
|
||||
md5.update(nonce + username + hash_password, 'utf8');
|
||||
key = md5.digest('hex');
|
||||
}
|
||||
|
||||
// Execute command
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(
|
||||
self.bson,
|
||||
f('%s.$cmd', db),
|
||||
{
|
||||
authenticate: 1,
|
||||
user: username,
|
||||
nonce: nonce,
|
||||
key: key
|
||||
},
|
||||
{
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}
|
||||
),
|
||||
function(err, r) {
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
executeMongoCR(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
MongoCR.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = MongoCR;
|
183
node_modules/mongodb-core/lib/auth/plain.js
generated
vendored
Normal file
183
node_modules/mongodb-core/lib/auth/plain.js
generated
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Binary = BSON.Binary;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new Plain authentication mechanism
|
||||
* @class
|
||||
* @return {Plain} A cursor instance
|
||||
*/
|
||||
var Plain = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Create payload
|
||||
var payload = new Binary(f('\x00%s\x00%s', username, password));
|
||||
|
||||
// Let's start the sasl process
|
||||
var command = {
|
||||
saslStart: 1,
|
||||
mechanism: 'PLAIN',
|
||||
payload: payload,
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
// Let's start the process
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
Plain.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = Plain;
|
442
node_modules/mongodb-core/lib/auth/scram.js
generated
vendored
Normal file
442
node_modules/mongodb-core/lib/auth/scram.js
generated
vendored
Normal file
@@ -0,0 +1,442 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
crypto = require('crypto'),
|
||||
retrieveBSON = require('../connection/utils').retrieveBSON,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError,
|
||||
Buffer = require('safe-buffer').Buffer;
|
||||
|
||||
let saslprep;
|
||||
|
||||
try {
|
||||
saslprep = require('saslprep');
|
||||
} catch (e) {
|
||||
// don't do anything;
|
||||
}
|
||||
|
||||
var BSON = retrieveBSON(),
|
||||
Binary = BSON.Binary;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
var id = 0;
|
||||
|
||||
/**
|
||||
* Creates a new ScramSHA authentication mechanism
|
||||
* @class
|
||||
* @return {ScramSHA} A cursor instance
|
||||
*/
|
||||
var ScramSHA = function(bson, cryptoMethod) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
this.id = id++;
|
||||
this.cryptoMethod = cryptoMethod || 'sha1';
|
||||
};
|
||||
|
||||
var parsePayload = function(payload) {
|
||||
var dict = {};
|
||||
var parts = payload.split(',');
|
||||
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var 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');
|
||||
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);
|
||||
const length = Math.max(a.length, b.length);
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
res.push(a[i] ^ b[i]);
|
||||
}
|
||||
|
||||
return Buffer.from(res).toString('base64');
|
||||
}
|
||||
|
||||
function H(method, text) {
|
||||
return crypto
|
||||
.createHash(method)
|
||||
.update(text)
|
||||
.digest();
|
||||
}
|
||||
|
||||
function HMAC(method, key, text) {
|
||||
return crypto
|
||||
.createHmac(method, key)
|
||||
.update(text)
|
||||
.digest();
|
||||
}
|
||||
|
||||
var _hiCache = {};
|
||||
var _hiCacheCount = 0;
|
||||
var _hiCachePurge = function() {
|
||||
_hiCache = {};
|
||||
_hiCacheCount = 0;
|
||||
};
|
||||
|
||||
const hiLengthMap = {
|
||||
sha256: 32,
|
||||
sha1: 20
|
||||
};
|
||||
|
||||
function HI(data, salt, iterations, cryptoMethod) {
|
||||
// omit the work if already generated
|
||||
const key = [data, salt.toString('base64'), iterations].join('_');
|
||||
if (_hiCache[key] !== undefined) {
|
||||
return _hiCache[key];
|
||||
}
|
||||
|
||||
// generate the salt
|
||||
const saltedData = crypto.pbkdf2Sync(
|
||||
data,
|
||||
salt,
|
||||
iterations,
|
||||
hiLengthMap[cryptoMethod],
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
// cache a copy to speed up the next lookup, but prevent unbounded cache growth
|
||||
if (_hiCacheCount >= 200) {
|
||||
_hiCachePurge();
|
||||
}
|
||||
|
||||
_hiCache[key] = saltedData;
|
||||
_hiCacheCount += 1;
|
||||
return saltedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
ScramSHA.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
const cryptoMethod = this.cryptoMethod;
|
||||
let mechanism = 'SCRAM-SHA-1';
|
||||
let processedPassword;
|
||||
|
||||
if (cryptoMethod === 'sha256') {
|
||||
mechanism = 'SCRAM-SHA-256';
|
||||
|
||||
let saslprepFn = (server.s && server.s.saslprep) || saslprep;
|
||||
|
||||
if (saslprepFn) {
|
||||
processedPassword = saslprepFn(password);
|
||||
} else {
|
||||
console.warn('Warning: no saslprep library specified. Passwords will not be sanitized');
|
||||
processedPassword = password;
|
||||
}
|
||||
} else {
|
||||
processedPassword = passwordDigest(username, password);
|
||||
}
|
||||
|
||||
// Execute MongoCR
|
||||
var executeScram = function(connection) {
|
||||
// Clean up the user
|
||||
username = username.replace('=', '=3D').replace(',', '=2C');
|
||||
|
||||
// Create a random nonce
|
||||
var nonce = crypto.randomBytes(24).toString('base64');
|
||||
// var nonce = 'MsQUY9iw0T9fx2MUEz6LZPwGuhVvWAhc'
|
||||
|
||||
// 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
|
||||
var cmd = {
|
||||
saslStart: 1,
|
||||
mechanism: mechanism,
|
||||
payload: new Binary(Buffer.concat([Buffer.from('n,,', 'utf8'), firstBare])),
|
||||
autoAuthorize: 1
|
||||
};
|
||||
|
||||
// Handle the error
|
||||
var handleError = function(err, r) {
|
||||
if (err) {
|
||||
numberOfValidConnections = numberOfValidConnections - 1;
|
||||
errorObject = err;
|
||||
return false;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
return false;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
return false;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Finish up
|
||||
var finish = function(_count, _numberOfValidConnections) {
|
||||
if (_count === 0 && _numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
return callback(null, true);
|
||||
} else if (_count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using scram'));
|
||||
return callback(errorObject, false);
|
||||
}
|
||||
};
|
||||
|
||||
var handleEnd = function(_err, _r) {
|
||||
// Handle any error
|
||||
handleError(_err, _r);
|
||||
// Adjust the number of connections
|
||||
count = count - 1;
|
||||
// Execute the finish
|
||||
finish(count, numberOfValidConnections);
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Do we have an error, handle it
|
||||
if (handleError(err, r) === false) {
|
||||
count = count - 1;
|
||||
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
return callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using scram'));
|
||||
return callback(errorObject, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the dictionary
|
||||
var dict = parsePayload(r.result.payload.value());
|
||||
|
||||
// Unpack dictionary
|
||||
var iterations = parseInt(dict.i, 10);
|
||||
var salt = dict.s;
|
||||
var rnonce = dict.r;
|
||||
|
||||
// Set up start of proof
|
||||
var withoutProof = f('c=biws,r=%s', rnonce);
|
||||
var saltedPassword = HI(
|
||||
processedPassword,
|
||||
Buffer.from(salt, 'base64'),
|
||||
iterations,
|
||||
cryptoMethod
|
||||
);
|
||||
|
||||
if (iterations && iterations < 4096) {
|
||||
const error = new MongoError(`Server returned an invalid iteration count ${iterations}`);
|
||||
return callback(error, false);
|
||||
}
|
||||
|
||||
// Create the client key
|
||||
const clientKey = HMAC(cryptoMethod, saltedPassword, 'Client Key');
|
||||
|
||||
// Create the stored key
|
||||
const storedKey = H(cryptoMethod, clientKey);
|
||||
|
||||
// Create the authentication message
|
||||
const authMessage = [
|
||||
firstBare,
|
||||
r.result.payload.value().toString('base64'),
|
||||
withoutProof
|
||||
].join(',');
|
||||
|
||||
// Create client signature
|
||||
const clientSignature = HMAC(cryptoMethod, storedKey, authMessage);
|
||||
|
||||
// Create client proof
|
||||
const clientProof = f('p=%s', xor(clientKey, clientSignature));
|
||||
|
||||
// Create client final
|
||||
const clientFinal = [withoutProof, clientProof].join(',');
|
||||
|
||||
// Create continue message
|
||||
const cmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.result.conversationId,
|
||||
payload: new Binary(Buffer.from(clientFinal))
|
||||
};
|
||||
|
||||
//
|
||||
// Execute sasl continue
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
if (r && r.result.done === false) {
|
||||
var cmd = {
|
||||
saslContinue: 1,
|
||||
conversationId: r.result.conversationId,
|
||||
payload: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
// Write the commmand on the connection
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, f('%s.$cmd', db), cmd, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
handleEnd(err, r);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
handleEnd(err, r);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
executeScram(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
ScramSHA.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
ScramSHA.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
// No connections
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
class ScramSHA1 extends ScramSHA {
|
||||
constructor(bson) {
|
||||
super(bson, 'sha1');
|
||||
}
|
||||
}
|
||||
|
||||
class ScramSHA256 extends ScramSHA {
|
||||
constructor(bson) {
|
||||
super(bson, 'sha256');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ScramSHA1, ScramSHA256 };
|
262
node_modules/mongodb-core/lib/auth/sspi.js
generated
vendored
Normal file
262
node_modules/mongodb-core/lib/auth/sspi.js
generated
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
'use strict';
|
||||
|
||||
const f = require('util').format;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const retrieveKerberos = require('../utils').retrieveKerberos;
|
||||
|
||||
var AuthSession = function(db, username, password, options) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new SSPI authentication mechanism
|
||||
* @class
|
||||
* @return {SSPI} A cursor instance
|
||||
*/
|
||||
var SSPI = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.auth = function(server, connections, db, username, password, options, callback) {
|
||||
var self = this;
|
||||
let kerberos;
|
||||
try {
|
||||
kerberos = retrieveKerberos();
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
var gssapiServiceName = options['gssapiServiceName'] || 'mongodb';
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Start Auth process for a connection
|
||||
SSIPAuthenticate(
|
||||
self,
|
||||
kerberos.processes.MongoAuthProcess,
|
||||
username,
|
||||
password,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
connection,
|
||||
options,
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r && typeof r === 'object' && r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r && typeof r === 'object' && r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password, options));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
function SSIPAuthenticate(
|
||||
self,
|
||||
MongoAuthProcess,
|
||||
username,
|
||||
password,
|
||||
gssapiServiceName,
|
||||
server,
|
||||
connection,
|
||||
options,
|
||||
callback
|
||||
) {
|
||||
const authProcess = new MongoAuthProcess(
|
||||
connection.host,
|
||||
connection.port,
|
||||
gssapiServiceName,
|
||||
options
|
||||
);
|
||||
|
||||
function authCommand(command, authCb) {
|
||||
const query = new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
});
|
||||
|
||||
server(connection, query, authCb);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
authCommand(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
|
||||
};
|
||||
|
||||
authCommand(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
|
||||
};
|
||||
|
||||
authCommand(command, (err, response) => {
|
||||
if (err) return callback(err, false);
|
||||
|
||||
authProcess.transition(null, err => {
|
||||
if (err) return callback(err, null);
|
||||
callback(null, response);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
SSPI.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
authStore[i].options,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = SSPI;
|
179
node_modules/mongodb-core/lib/auth/x509.js
generated
vendored
Normal file
179
node_modules/mongodb-core/lib/auth/x509.js
generated
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
Query = require('../connection/commands').Query,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var AuthSession = function(db, username, password) {
|
||||
this.db = db;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
};
|
||||
|
||||
AuthSession.prototype.equal = function(session) {
|
||||
return (
|
||||
session.db === this.db &&
|
||||
session.username === this.username &&
|
||||
session.password === this.password
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new X509 authentication mechanism
|
||||
* @class
|
||||
* @return {X509} A cursor instance
|
||||
*/
|
||||
var X509 = function(bson) {
|
||||
this.bson = bson;
|
||||
this.authStore = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {string} db Name of the database
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.auth = function(server, connections, db, username, password, callback) {
|
||||
var self = this;
|
||||
// Total connections
|
||||
var count = connections.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
|
||||
// Valid connections
|
||||
var numberOfValidConnections = 0;
|
||||
var errorObject = null;
|
||||
|
||||
// For each connection we need to authenticate
|
||||
while (connections.length > 0) {
|
||||
// Execute MongoCR
|
||||
var execute = function(connection) {
|
||||
// Let's start the sasl process
|
||||
var command = {
|
||||
authenticate: 1,
|
||||
mechanism: 'MONGODB-X509'
|
||||
};
|
||||
|
||||
// Add username if specified
|
||||
if (username) {
|
||||
command.user = username;
|
||||
}
|
||||
|
||||
// Let's start the process
|
||||
server(
|
||||
connection,
|
||||
new Query(self.bson, '$external.$cmd', command, {
|
||||
numberToSkip: 0,
|
||||
numberToReturn: 1
|
||||
}),
|
||||
function(err, r) {
|
||||
// Adjust count
|
||||
count = count - 1;
|
||||
|
||||
// If we have an error
|
||||
if (err) {
|
||||
errorObject = err;
|
||||
} else if (r.result['$err']) {
|
||||
errorObject = r.result;
|
||||
} else if (r.result['errmsg']) {
|
||||
errorObject = r.result;
|
||||
} else {
|
||||
numberOfValidConnections = numberOfValidConnections + 1;
|
||||
}
|
||||
|
||||
// We have authenticated all connections
|
||||
if (count === 0 && numberOfValidConnections > 0) {
|
||||
// Store the auth details
|
||||
addAuthSession(self.authStore, new AuthSession(db, username, password));
|
||||
// Return correct authentication
|
||||
callback(null, true);
|
||||
} else if (count === 0) {
|
||||
if (errorObject == null)
|
||||
errorObject = new MongoError(f('failed to authenticate using mongocr'));
|
||||
callback(errorObject, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _execute = function(_connection) {
|
||||
process.nextTick(function() {
|
||||
execute(_connection);
|
||||
});
|
||||
};
|
||||
|
||||
_execute(connections.shift());
|
||||
}
|
||||
};
|
||||
|
||||
// Add to store only if it does not exist
|
||||
var addAuthSession = function(authStore, session) {
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
if (authStore[i].equal(session)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) authStore.push(session);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authStore credentials
|
||||
* @method
|
||||
* @param {string} db Name of database we are removing authStore details about
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.logout = function(dbName) {
|
||||
this.authStore = this.authStore.filter(function(x) {
|
||||
return x.db !== dbName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Re authenticate pool
|
||||
* @method
|
||||
* @param {{Server}|{ReplSet}|{Mongos}} server Topology the authentication method is being called on
|
||||
* @param {[]Connections} connections Connections to authenticate using this authenticator
|
||||
* @param {authResultCallback} callback The callback to return the result from the authentication
|
||||
* @return {object}
|
||||
*/
|
||||
X509.prototype.reauthenticate = function(server, connections, callback) {
|
||||
var authStore = this.authStore.slice(0);
|
||||
var count = authStore.length;
|
||||
if (count === 0) return callback(null, null);
|
||||
// Iterate over all the auth details stored
|
||||
for (var i = 0; i < authStore.length; i++) {
|
||||
this.auth(
|
||||
server,
|
||||
connections,
|
||||
authStore[i].db,
|
||||
authStore[i].username,
|
||||
authStore[i].password,
|
||||
function(err) {
|
||||
count = count - 1;
|
||||
// Done re-authenticating
|
||||
if (count === 0) {
|
||||
callback(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = X509;
|
228
node_modules/mongodb-core/lib/connection/apm.js
generated
vendored
Normal file
228
node_modules/mongodb-core/lib/connection/apm.js
generated
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
'use strict';
|
||||
const KillCursor = require('../connection/commands').KillCursor;
|
||||
const GetMore = require('../connection/commands').GetMore;
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
|
||||
/** Commands that we want to redact because of the sensitive nature of their contents */
|
||||
const SENSITIVE_COMMANDS = new Set([
|
||||
'authenticate',
|
||||
'saslStart',
|
||||
'saslContinue',
|
||||
'getnonce',
|
||||
'createUser',
|
||||
'updateUser',
|
||||
'copydbgetnonce',
|
||||
'copydbsaslstart',
|
||||
'copydb'
|
||||
]);
|
||||
|
||||
// helper methods
|
||||
const extractCommandName = command => Object.keys(command)[0];
|
||||
const namespace = command => command.ns;
|
||||
const databaseName = command => command.ns.split('.')[0];
|
||||
const collectionName = command => command.ns.split('.')[1];
|
||||
const generateConnectionId = pool => `${pool.options.host}:${pool.options.port}`;
|
||||
const maybeRedact = (commandName, result) => (SENSITIVE_COMMANDS.has(commandName) ? {} : result);
|
||||
|
||||
const LEGACY_FIND_QUERY_MAP = {
|
||||
$query: 'filter',
|
||||
$orderby: 'sort',
|
||||
$hint: 'hint',
|
||||
$comment: 'comment',
|
||||
$maxScan: 'maxScan',
|
||||
$max: 'max',
|
||||
$min: 'min',
|
||||
$returnKey: 'returnKey',
|
||||
$showDiskLoc: 'showRecordId',
|
||||
$maxTimeMS: 'maxTimeMS',
|
||||
$snapshot: 'snapshot'
|
||||
};
|
||||
|
||||
const LEGACY_FIND_OPTIONS_MAP = {
|
||||
numberToSkip: 'skip',
|
||||
numberToReturn: 'batchSize',
|
||||
returnFieldsSelector: 'projection'
|
||||
};
|
||||
|
||||
const OP_QUERY_KEYS = [
|
||||
'tailable',
|
||||
'oplogReplay',
|
||||
'noCursorTimeout',
|
||||
'awaitData',
|
||||
'partial',
|
||||
'exhaust'
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract the actual command from the query, possibly upconverting if it's a legacy
|
||||
* format
|
||||
*
|
||||
* @param {Object} command the command
|
||||
*/
|
||||
const extractCommand = command => {
|
||||
if (command instanceof GetMore) {
|
||||
return {
|
||||
getMore: command.cursorId,
|
||||
collection: collectionName(command),
|
||||
batchSize: command.numberToReturn
|
||||
};
|
||||
}
|
||||
|
||||
if (command instanceof KillCursor) {
|
||||
return {
|
||||
killCursors: collectionName(command),
|
||||
cursors: command.cursorIds
|
||||
};
|
||||
}
|
||||
|
||||
if (command.query && command.query.$query) {
|
||||
let result;
|
||||
if (command.ns === 'admin.$cmd') {
|
||||
// upconvert legacy command
|
||||
result = Object.assign({}, command.query.$query);
|
||||
} else {
|
||||
// upconvert legacy find command
|
||||
result = { find: collectionName(command) };
|
||||
Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
|
||||
if (typeof command.query[key] !== 'undefined')
|
||||
result[LEGACY_FIND_QUERY_MAP[key]] = command.query[key];
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
|
||||
if (typeof command[key] !== 'undefined') result[LEGACY_FIND_OPTIONS_MAP[key]] = command[key];
|
||||
});
|
||||
|
||||
OP_QUERY_KEYS.forEach(key => {
|
||||
if (command[key]) result[key] = command[key];
|
||||
});
|
||||
|
||||
if (typeof command.pre32Limit !== 'undefined') {
|
||||
result.limit = command.pre32Limit;
|
||||
}
|
||||
|
||||
if (command.query.$explain) {
|
||||
return { explain: result };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return command.query ? command.query : command;
|
||||
};
|
||||
|
||||
const extractReply = (command, reply) => {
|
||||
if (command instanceof GetMore) {
|
||||
return {
|
||||
ok: 1,
|
||||
cursor: {
|
||||
id: reply.message.cursorId,
|
||||
ns: namespace(command),
|
||||
nextBatch: reply.message.documents
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (command instanceof KillCursor) {
|
||||
return {
|
||||
ok: 1,
|
||||
cursorsUnknown: command.cursorIds
|
||||
};
|
||||
}
|
||||
|
||||
// is this a legacy find command?
|
||||
if (command.query && typeof command.query.$query !== 'undefined') {
|
||||
return {
|
||||
ok: 1,
|
||||
cursor: {
|
||||
id: reply.message.cursorId,
|
||||
ns: namespace(command),
|
||||
firstBatch: reply.message.documents
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return reply.result;
|
||||
};
|
||||
|
||||
/** An event indicating the start of a given command */
|
||||
class CommandStartedEvent {
|
||||
/**
|
||||
* Create a started event
|
||||
*
|
||||
* @param {Pool} pool the pool that originated the command
|
||||
* @param {Object} command the command
|
||||
*/
|
||||
constructor(pool, command) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
|
||||
// NOTE: remove in major revision, this is not spec behavior
|
||||
if (SENSITIVE_COMMANDS.has(commandName)) {
|
||||
this.commandObj = {};
|
||||
this.commandObj[commandName] = true;
|
||||
}
|
||||
|
||||
Object.assign(this, {
|
||||
command: cmd,
|
||||
databaseName: databaseName(command),
|
||||
commandName,
|
||||
requestId: command.requestId,
|
||||
connectionId: generateConnectionId(pool)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** An event indicating the success of a given command */
|
||||
class CommandSucceededEvent {
|
||||
/**
|
||||
* Create a succeeded event
|
||||
*
|
||||
* @param {Pool} pool the pool that originated the command
|
||||
* @param {Object} command the command
|
||||
* @param {Object} reply the reply for this command from the server
|
||||
* @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
|
||||
*/
|
||||
constructor(pool, command, reply, started) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
|
||||
Object.assign(this, {
|
||||
duration: calculateDurationInMs(started),
|
||||
commandName,
|
||||
reply: maybeRedact(commandName, extractReply(command, reply)),
|
||||
requestId: command.requestId,
|
||||
connectionId: generateConnectionId(pool)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** An event indicating the failure of a given command */
|
||||
class CommandFailedEvent {
|
||||
/**
|
||||
* Create a failure event
|
||||
*
|
||||
* @param {Pool} pool the pool that originated the command
|
||||
* @param {Object} command the command
|
||||
* @param {MongoError|Object} error the generated error or a server error response
|
||||
* @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
|
||||
*/
|
||||
constructor(pool, command, error, started) {
|
||||
const cmd = extractCommand(command);
|
||||
const commandName = extractCommandName(cmd);
|
||||
|
||||
Object.assign(this, {
|
||||
duration: calculateDurationInMs(started),
|
||||
commandName,
|
||||
failure: maybeRedact(commandName, error),
|
||||
requestId: command.requestId,
|
||||
connectionId: generateConnectionId(pool)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CommandStartedEvent,
|
||||
CommandSucceededEvent,
|
||||
CommandFailedEvent
|
||||
};
|
34
node_modules/mongodb-core/lib/connection/command_result.js
generated
vendored
Normal file
34
node_modules/mongodb-core/lib/connection/command_result.js
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Creates a new CommandResult instance
|
||||
* @class
|
||||
* @param {object} result CommandResult object
|
||||
* @param {Connection} connection A connection instance associated with this result
|
||||
* @return {CommandResult} A cursor instance
|
||||
*/
|
||||
var CommandResult = function(result, connection, message) {
|
||||
this.result = result;
|
||||
this.connection = connection;
|
||||
this.message = message;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert CommandResult to JSON
|
||||
* @method
|
||||
* @return {object}
|
||||
*/
|
||||
CommandResult.prototype.toJSON = function() {
|
||||
return this.result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert CommandResult to String representation
|
||||
* @method
|
||||
* @return {string}
|
||||
*/
|
||||
CommandResult.prototype.toString = function() {
|
||||
return JSON.stringify(this.toJSON());
|
||||
};
|
||||
|
||||
module.exports = CommandResult;
|
546
node_modules/mongodb-core/lib/connection/commands.js
generated
vendored
Normal file
546
node_modules/mongodb-core/lib/connection/commands.js
generated
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
'use strict';
|
||||
|
||||
var retrieveBSON = require('./utils').retrieveBSON;
|
||||
var BSON = retrieveBSON();
|
||||
var Long = BSON.Long;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const Buffer = require('safe-buffer').Buffer;
|
||||
|
||||
// Incrementing request id
|
||||
var _requestId = 0;
|
||||
|
||||
// Wire command operation ids
|
||||
var opcodes = require('../wireprotocol/shared').opcodes;
|
||||
|
||||
// Query flags
|
||||
var OPTS_TAILABLE_CURSOR = 2;
|
||||
var OPTS_SLAVE = 4;
|
||||
var OPTS_OPLOG_REPLAY = 8;
|
||||
var OPTS_NO_CURSOR_TIMEOUT = 16;
|
||||
var OPTS_AWAIT_DATA = 32;
|
||||
var OPTS_EXHAUST = 64;
|
||||
var OPTS_PARTIAL = 128;
|
||||
|
||||
// Response flags
|
||||
var CURSOR_NOT_FOUND = 1;
|
||||
var QUERY_FAILURE = 2;
|
||||
var SHARD_CONFIG_STALE = 4;
|
||||
var AWAIT_CAPABLE = 8;
|
||||
|
||||
/**************************************************************
|
||||
* QUERY
|
||||
**************************************************************/
|
||||
var Query = function(bson, ns, query, options) {
|
||||
var self = this;
|
||||
// Basic options needed to be passed in
|
||||
if (ns == null) throw new Error('ns must be specified for query');
|
||||
if (query == null) throw new Error('query must be specified for query');
|
||||
|
||||
// Validate that we are not passing 0x00 in the collection name
|
||||
if (ns.indexOf('\x00') !== -1) {
|
||||
throw new Error('namespace cannot contain a null character');
|
||||
}
|
||||
|
||||
// Basic options
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.query = query;
|
||||
|
||||
// Additional options
|
||||
this.numberToSkip = options.numberToSkip || 0;
|
||||
this.numberToReturn = options.numberToReturn || 0;
|
||||
this.returnFieldSelector = options.returnFieldSelector || null;
|
||||
this.requestId = Query.getRequestId();
|
||||
|
||||
// special case for pre-3.2 find commands, delete ASAP
|
||||
this.pre32Limit = options.pre32Limit;
|
||||
|
||||
// Serialization option
|
||||
this.serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
this.ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
this.maxBsonSize = options.maxBsonSize || 1024 * 1024 * 16;
|
||||
this.checkKeys = typeof options.checkKeys === 'boolean' ? options.checkKeys : true;
|
||||
this.batchSize = self.numberToReturn;
|
||||
|
||||
// Flags
|
||||
this.tailable = false;
|
||||
this.slaveOk = typeof options.slaveOk === 'boolean' ? options.slaveOk : false;
|
||||
this.oplogReplay = false;
|
||||
this.noCursorTimeout = false;
|
||||
this.awaitData = false;
|
||||
this.exhaust = false;
|
||||
this.partial = false;
|
||||
};
|
||||
|
||||
//
|
||||
// Assign a new request Id
|
||||
Query.prototype.incRequestId = function() {
|
||||
this.requestId = _requestId++;
|
||||
};
|
||||
|
||||
//
|
||||
// Assign a new request Id
|
||||
Query.nextRequestId = function() {
|
||||
return _requestId + 1;
|
||||
};
|
||||
|
||||
//
|
||||
// Uses a single allocated buffer for the process, avoiding multiple memory allocations
|
||||
Query.prototype.toBin = function() {
|
||||
var self = this;
|
||||
var buffers = [];
|
||||
var projection = null;
|
||||
|
||||
// Set up the flags
|
||||
var flags = 0;
|
||||
if (this.tailable) {
|
||||
flags |= OPTS_TAILABLE_CURSOR;
|
||||
}
|
||||
|
||||
if (this.slaveOk) {
|
||||
flags |= OPTS_SLAVE;
|
||||
}
|
||||
|
||||
if (this.oplogReplay) {
|
||||
flags |= OPTS_OPLOG_REPLAY;
|
||||
}
|
||||
|
||||
if (this.noCursorTimeout) {
|
||||
flags |= OPTS_NO_CURSOR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (this.awaitData) {
|
||||
flags |= OPTS_AWAIT_DATA;
|
||||
}
|
||||
|
||||
if (this.exhaust) {
|
||||
flags |= OPTS_EXHAUST;
|
||||
}
|
||||
|
||||
if (this.partial) {
|
||||
flags |= OPTS_PARTIAL;
|
||||
}
|
||||
|
||||
// If batchSize is different to self.numberToReturn
|
||||
if (self.batchSize !== self.numberToReturn) self.numberToReturn = self.batchSize;
|
||||
|
||||
// Allocate write protocol header buffer
|
||||
var header = Buffer.alloc(
|
||||
4 * 4 + // Header
|
||||
4 + // Flags
|
||||
Buffer.byteLength(self.ns) +
|
||||
1 + // namespace
|
||||
4 + // numberToSkip
|
||||
4 // numberToReturn
|
||||
);
|
||||
|
||||
// Add header to buffers
|
||||
buffers.push(header);
|
||||
|
||||
// Serialize the query
|
||||
var query = self.bson.serialize(this.query, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
|
||||
// Add query document
|
||||
buffers.push(query);
|
||||
|
||||
if (self.returnFieldSelector && Object.keys(self.returnFieldSelector).length > 0) {
|
||||
// Serialize the projection document
|
||||
projection = self.bson.serialize(this.returnFieldSelector, {
|
||||
checkKeys: this.checkKeys,
|
||||
serializeFunctions: this.serializeFunctions,
|
||||
ignoreUndefined: this.ignoreUndefined
|
||||
});
|
||||
// Add projection document
|
||||
buffers.push(projection);
|
||||
}
|
||||
|
||||
// Total message size
|
||||
var totalLength = header.length + query.length + (projection ? projection.length : 0);
|
||||
|
||||
// Set up the index
|
||||
var index = 4;
|
||||
|
||||
// Write total document length
|
||||
header[3] = (totalLength >> 24) & 0xff;
|
||||
header[2] = (totalLength >> 16) & 0xff;
|
||||
header[1] = (totalLength >> 8) & 0xff;
|
||||
header[0] = totalLength & 0xff;
|
||||
|
||||
// Write header information requestId
|
||||
header[index + 3] = (this.requestId >> 24) & 0xff;
|
||||
header[index + 2] = (this.requestId >> 16) & 0xff;
|
||||
header[index + 1] = (this.requestId >> 8) & 0xff;
|
||||
header[index] = this.requestId & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information responseTo
|
||||
header[index + 3] = (0 >> 24) & 0xff;
|
||||
header[index + 2] = (0 >> 16) & 0xff;
|
||||
header[index + 1] = (0 >> 8) & 0xff;
|
||||
header[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information OP_QUERY
|
||||
header[index + 3] = (opcodes.OP_QUERY >> 24) & 0xff;
|
||||
header[index + 2] = (opcodes.OP_QUERY >> 16) & 0xff;
|
||||
header[index + 1] = (opcodes.OP_QUERY >> 8) & 0xff;
|
||||
header[index] = opcodes.OP_QUERY & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information flags
|
||||
header[index + 3] = (flags >> 24) & 0xff;
|
||||
header[index + 2] = (flags >> 16) & 0xff;
|
||||
header[index + 1] = (flags >> 8) & 0xff;
|
||||
header[index] = flags & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write collection name
|
||||
index = index + header.write(this.ns, index, 'utf8') + 1;
|
||||
header[index - 1] = 0;
|
||||
|
||||
// Write header information flags numberToSkip
|
||||
header[index + 3] = (this.numberToSkip >> 24) & 0xff;
|
||||
header[index + 2] = (this.numberToSkip >> 16) & 0xff;
|
||||
header[index + 1] = (this.numberToSkip >> 8) & 0xff;
|
||||
header[index] = this.numberToSkip & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write header information flags numberToReturn
|
||||
header[index + 3] = (this.numberToReturn >> 24) & 0xff;
|
||||
header[index + 2] = (this.numberToReturn >> 16) & 0xff;
|
||||
header[index + 1] = (this.numberToReturn >> 8) & 0xff;
|
||||
header[index] = this.numberToReturn & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Return the buffers
|
||||
return buffers;
|
||||
};
|
||||
|
||||
Query.getRequestId = function() {
|
||||
return ++_requestId;
|
||||
};
|
||||
|
||||
/**************************************************************
|
||||
* GETMORE
|
||||
**************************************************************/
|
||||
var GetMore = function(bson, ns, cursorId, opts) {
|
||||
opts = opts || {};
|
||||
this.numberToReturn = opts.numberToReturn || 0;
|
||||
this.requestId = _requestId++;
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.cursorId = cursorId;
|
||||
};
|
||||
|
||||
//
|
||||
// Uses a single allocated buffer for the process, avoiding multiple memory allocations
|
||||
GetMore.prototype.toBin = function() {
|
||||
var length = 4 + Buffer.byteLength(this.ns) + 1 + 4 + 8 + 4 * 4;
|
||||
// Create command buffer
|
||||
var index = 0;
|
||||
// Allocate buffer
|
||||
var _buffer = Buffer.alloc(length);
|
||||
|
||||
// Write header information
|
||||
// index = write32bit(index, _buffer, length);
|
||||
_buffer[index + 3] = (length >> 24) & 0xff;
|
||||
_buffer[index + 2] = (length >> 16) & 0xff;
|
||||
_buffer[index + 1] = (length >> 8) & 0xff;
|
||||
_buffer[index] = length & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, requestId);
|
||||
_buffer[index + 3] = (this.requestId >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.requestId >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.requestId >> 8) & 0xff;
|
||||
_buffer[index] = this.requestId & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, 0);
|
||||
_buffer[index + 3] = (0 >> 24) & 0xff;
|
||||
_buffer[index + 2] = (0 >> 16) & 0xff;
|
||||
_buffer[index + 1] = (0 >> 8) & 0xff;
|
||||
_buffer[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, OP_GETMORE);
|
||||
_buffer[index + 3] = (opcodes.OP_GETMORE >> 24) & 0xff;
|
||||
_buffer[index + 2] = (opcodes.OP_GETMORE >> 16) & 0xff;
|
||||
_buffer[index + 1] = (opcodes.OP_GETMORE >> 8) & 0xff;
|
||||
_buffer[index] = opcodes.OP_GETMORE & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, 0);
|
||||
_buffer[index + 3] = (0 >> 24) & 0xff;
|
||||
_buffer[index + 2] = (0 >> 16) & 0xff;
|
||||
_buffer[index + 1] = (0 >> 8) & 0xff;
|
||||
_buffer[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write collection name
|
||||
index = index + _buffer.write(this.ns, index, 'utf8') + 1;
|
||||
_buffer[index - 1] = 0;
|
||||
|
||||
// Write batch size
|
||||
// index = write32bit(index, _buffer, numberToReturn);
|
||||
_buffer[index + 3] = (this.numberToReturn >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.numberToReturn >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.numberToReturn >> 8) & 0xff;
|
||||
_buffer[index] = this.numberToReturn & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write cursor id
|
||||
// index = write32bit(index, _buffer, cursorId.getLowBits());
|
||||
_buffer[index + 3] = (this.cursorId.getLowBits() >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.cursorId.getLowBits() >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.cursorId.getLowBits() >> 8) & 0xff;
|
||||
_buffer[index] = this.cursorId.getLowBits() & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, cursorId.getHighBits());
|
||||
_buffer[index + 3] = (this.cursorId.getHighBits() >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.cursorId.getHighBits() >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.cursorId.getHighBits() >> 8) & 0xff;
|
||||
_buffer[index] = this.cursorId.getHighBits() & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Return buffer
|
||||
return _buffer;
|
||||
};
|
||||
|
||||
/**************************************************************
|
||||
* KILLCURSOR
|
||||
**************************************************************/
|
||||
var KillCursor = function(bson, ns, cursorIds) {
|
||||
this.ns = ns;
|
||||
this.requestId = _requestId++;
|
||||
this.cursorIds = cursorIds;
|
||||
};
|
||||
|
||||
//
|
||||
// Uses a single allocated buffer for the process, avoiding multiple memory allocations
|
||||
KillCursor.prototype.toBin = function() {
|
||||
var length = 4 + 4 + 4 * 4 + this.cursorIds.length * 8;
|
||||
|
||||
// Create command buffer
|
||||
var index = 0;
|
||||
var _buffer = Buffer.alloc(length);
|
||||
|
||||
// Write header information
|
||||
// index = write32bit(index, _buffer, length);
|
||||
_buffer[index + 3] = (length >> 24) & 0xff;
|
||||
_buffer[index + 2] = (length >> 16) & 0xff;
|
||||
_buffer[index + 1] = (length >> 8) & 0xff;
|
||||
_buffer[index] = length & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, requestId);
|
||||
_buffer[index + 3] = (this.requestId >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.requestId >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.requestId >> 8) & 0xff;
|
||||
_buffer[index] = this.requestId & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, 0);
|
||||
_buffer[index + 3] = (0 >> 24) & 0xff;
|
||||
_buffer[index + 2] = (0 >> 16) & 0xff;
|
||||
_buffer[index + 1] = (0 >> 8) & 0xff;
|
||||
_buffer[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, OP_KILL_CURSORS);
|
||||
_buffer[index + 3] = (opcodes.OP_KILL_CURSORS >> 24) & 0xff;
|
||||
_buffer[index + 2] = (opcodes.OP_KILL_CURSORS >> 16) & 0xff;
|
||||
_buffer[index + 1] = (opcodes.OP_KILL_CURSORS >> 8) & 0xff;
|
||||
_buffer[index] = opcodes.OP_KILL_CURSORS & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, 0);
|
||||
_buffer[index + 3] = (0 >> 24) & 0xff;
|
||||
_buffer[index + 2] = (0 >> 16) & 0xff;
|
||||
_buffer[index + 1] = (0 >> 8) & 0xff;
|
||||
_buffer[index] = 0 & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write batch size
|
||||
// index = write32bit(index, _buffer, this.cursorIds.length);
|
||||
_buffer[index + 3] = (this.cursorIds.length >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.cursorIds.length >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.cursorIds.length >> 8) & 0xff;
|
||||
_buffer[index] = this.cursorIds.length & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// Write all the cursor ids into the array
|
||||
for (var i = 0; i < this.cursorIds.length; i++) {
|
||||
// Write cursor id
|
||||
// index = write32bit(index, _buffer, cursorIds[i].getLowBits());
|
||||
_buffer[index + 3] = (this.cursorIds[i].getLowBits() >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.cursorIds[i].getLowBits() >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.cursorIds[i].getLowBits() >> 8) & 0xff;
|
||||
_buffer[index] = this.cursorIds[i].getLowBits() & 0xff;
|
||||
index = index + 4;
|
||||
|
||||
// index = write32bit(index, _buffer, cursorIds[i].getHighBits());
|
||||
_buffer[index + 3] = (this.cursorIds[i].getHighBits() >> 24) & 0xff;
|
||||
_buffer[index + 2] = (this.cursorIds[i].getHighBits() >> 16) & 0xff;
|
||||
_buffer[index + 1] = (this.cursorIds[i].getHighBits() >> 8) & 0xff;
|
||||
_buffer[index] = this.cursorIds[i].getHighBits() & 0xff;
|
||||
index = index + 4;
|
||||
}
|
||||
|
||||
// Return buffer
|
||||
return _buffer;
|
||||
};
|
||||
|
||||
var Response = function(bson, message, msgHeader, msgBody, opts) {
|
||||
opts = opts || { promoteLongs: true, promoteValues: true, promoteBuffers: false };
|
||||
this.parsed = false;
|
||||
this.raw = message;
|
||||
this.data = msgBody;
|
||||
this.bson = bson;
|
||||
this.opts = opts;
|
||||
|
||||
// Read the message header
|
||||
this.length = msgHeader.length;
|
||||
this.requestId = msgHeader.requestId;
|
||||
this.responseTo = msgHeader.responseTo;
|
||||
this.opCode = msgHeader.opCode;
|
||||
this.fromCompressed = msgHeader.fromCompressed;
|
||||
|
||||
// Read the message body
|
||||
this.responseFlags = msgBody.readInt32LE(0);
|
||||
this.cursorId = new Long(msgBody.readInt32LE(4), msgBody.readInt32LE(8));
|
||||
this.startingFrom = msgBody.readInt32LE(12);
|
||||
this.numberReturned = msgBody.readInt32LE(16);
|
||||
|
||||
// Preallocate document array
|
||||
this.documents = new Array(this.numberReturned);
|
||||
|
||||
// Flag values
|
||||
this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) !== 0;
|
||||
this.queryFailure = (this.responseFlags & QUERY_FAILURE) !== 0;
|
||||
this.shardConfigStale = (this.responseFlags & SHARD_CONFIG_STALE) !== 0;
|
||||
this.awaitCapable = (this.responseFlags & AWAIT_CAPABLE) !== 0;
|
||||
this.promoteLongs = typeof opts.promoteLongs === 'boolean' ? opts.promoteLongs : true;
|
||||
this.promoteValues = typeof opts.promoteValues === 'boolean' ? opts.promoteValues : true;
|
||||
this.promoteBuffers = typeof opts.promoteBuffers === 'boolean' ? opts.promoteBuffers : false;
|
||||
};
|
||||
|
||||
Response.prototype.isParsed = function() {
|
||||
return this.parsed;
|
||||
};
|
||||
|
||||
Response.prototype.parse = function(options) {
|
||||
// Don't parse again if not needed
|
||||
if (this.parsed) return;
|
||||
options = options || {};
|
||||
|
||||
// Allow the return of raw documents instead of parsing
|
||||
var raw = options.raw || false;
|
||||
var documentsReturnedIn = options.documentsReturnedIn || null;
|
||||
var promoteLongs =
|
||||
typeof options.promoteLongs === 'boolean' ? options.promoteLongs : this.opts.promoteLongs;
|
||||
var promoteValues =
|
||||
typeof options.promoteValues === 'boolean' ? options.promoteValues : this.opts.promoteValues;
|
||||
var promoteBuffers =
|
||||
typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : this.opts.promoteBuffers;
|
||||
var bsonSize, _options;
|
||||
|
||||
// Set up the options
|
||||
_options = {
|
||||
promoteLongs: promoteLongs,
|
||||
promoteValues: promoteValues,
|
||||
promoteBuffers: promoteBuffers
|
||||
};
|
||||
|
||||
// Position within OP_REPLY at which documents start
|
||||
// (See https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#wire-op-reply)
|
||||
this.index = 20;
|
||||
|
||||
//
|
||||
// Single document and documentsReturnedIn set
|
||||
//
|
||||
if (this.numberReturned === 1 && documentsReturnedIn != null && raw) {
|
||||
// Calculate the bson size
|
||||
bsonSize =
|
||||
this.data[this.index] |
|
||||
(this.data[this.index + 1] << 8) |
|
||||
(this.data[this.index + 2] << 16) |
|
||||
(this.data[this.index + 3] << 24);
|
||||
// Slice out the buffer containing the command result document
|
||||
var document = this.data.slice(this.index, this.index + bsonSize);
|
||||
// Set up field we wish to keep as raw
|
||||
var fieldsAsRaw = {};
|
||||
fieldsAsRaw[documentsReturnedIn] = true;
|
||||
_options.fieldsAsRaw = fieldsAsRaw;
|
||||
|
||||
// Deserialize but keep the array of documents in non-parsed form
|
||||
var doc = this.bson.deserialize(document, _options);
|
||||
|
||||
if (doc instanceof Error) {
|
||||
throw doc;
|
||||
}
|
||||
|
||||
if (doc.errmsg) {
|
||||
throw new MongoError(doc.errmsg);
|
||||
}
|
||||
|
||||
if (!doc.cursor) {
|
||||
throw new MongoError('Cursor not found');
|
||||
}
|
||||
|
||||
// Get the documents
|
||||
this.documents = doc.cursor[documentsReturnedIn];
|
||||
this.numberReturned = this.documents.length;
|
||||
// Ensure we have a Long valie cursor id
|
||||
this.cursorId =
|
||||
typeof doc.cursor.id === 'number' ? Long.fromNumber(doc.cursor.id) : doc.cursor.id;
|
||||
|
||||
// Adjust the index
|
||||
this.index = this.index + bsonSize;
|
||||
|
||||
// Set as parsed
|
||||
this.parsed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Parse Body
|
||||
//
|
||||
for (var i = 0; i < this.numberReturned; i++) {
|
||||
bsonSize =
|
||||
this.data[this.index] |
|
||||
(this.data[this.index + 1] << 8) |
|
||||
(this.data[this.index + 2] << 16) |
|
||||
(this.data[this.index + 3] << 24);
|
||||
|
||||
// If we have raw results specified slice the return document
|
||||
if (raw) {
|
||||
this.documents[i] = this.data.slice(this.index, this.index + bsonSize);
|
||||
} else {
|
||||
this.documents[i] = this.bson.deserialize(
|
||||
this.data.slice(this.index, this.index + bsonSize),
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
// Adjust the index
|
||||
this.index = this.index + bsonSize;
|
||||
}
|
||||
|
||||
// Set parsed
|
||||
this.parsed = true;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Query: Query,
|
||||
GetMore: GetMore,
|
||||
Response: Response,
|
||||
KillCursor: KillCursor
|
||||
};
|
805
node_modules/mongodb-core/lib/connection/connection.js
generated
vendored
Normal file
805
node_modules/mongodb-core/lib/connection/connection.js
generated
vendored
Normal file
@@ -0,0 +1,805 @@
|
||||
'use strict';
|
||||
|
||||
var inherits = require('util').inherits,
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
net = require('net'),
|
||||
tls = require('tls'),
|
||||
crypto = require('crypto'),
|
||||
f = require('util').format,
|
||||
debugOptions = require('./utils').debugOptions,
|
||||
parseHeader = require('../wireprotocol/shared').parseHeader,
|
||||
decompress = require('../wireprotocol/compression').decompress,
|
||||
Response = require('./commands').Response,
|
||||
MongoNetworkError = require('../error').MongoNetworkError,
|
||||
Logger = require('./logger'),
|
||||
OP_COMPRESSED = require('../wireprotocol/shared').opcodes.OP_COMPRESSED,
|
||||
MESSAGE_HEADER_SIZE = require('../wireprotocol/shared').MESSAGE_HEADER_SIZE,
|
||||
Buffer = require('safe-buffer').Buffer;
|
||||
|
||||
var _id = 0;
|
||||
var debugFields = [
|
||||
'host',
|
||||
'port',
|
||||
'size',
|
||||
'keepAlive',
|
||||
'keepAliveInitialDelay',
|
||||
'noDelay',
|
||||
'connectionTimeout',
|
||||
'socketTimeout',
|
||||
'singleBufferSerializtion',
|
||||
'ssl',
|
||||
'ca',
|
||||
'crl',
|
||||
'cert',
|
||||
'rejectUnauthorized',
|
||||
'promoteLongs',
|
||||
'promoteValues',
|
||||
'promoteBuffers',
|
||||
'checkServerIdentity'
|
||||
];
|
||||
|
||||
var connectionAccountingSpy = undefined;
|
||||
var connectionAccounting = false;
|
||||
var connections = {};
|
||||
|
||||
/**
|
||||
* Creates a new Connection instance
|
||||
* @class
|
||||
* @param {string} options.host The server host
|
||||
* @param {number} options.port The server port
|
||||
* @param {number} [options.family=null] IP version for DNS lookup, passed down to Node's [`dns.lookup()` function](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback). If set to `6`, will only look for ipv6 addresses.
|
||||
* @param {boolean} [options.keepAlive=true] TCP Connection keep alive enabled
|
||||
* @param {number} [options.keepAliveInitialDelay=300000] 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
|
||||
* @param {boolean} [options.singleBufferSerializtion=true] Serialize into single buffer, trade of peak memory for serialization speed
|
||||
* @param {boolean} [options.ssl=false] Use SSL for connection
|
||||
* @param {boolean|function} [options.checkServerIdentity=true] Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function.
|
||||
* @param {Buffer} [options.ca] SSL Certificate store binary buffer
|
||||
* @param {Buffer} [options.crl] SSL Certificate revocation store binary buffer
|
||||
* @param {Buffer} [options.cert] SSL Certificate binary buffer
|
||||
* @param {Buffer} [options.key] SSL Key file binary buffer
|
||||
* @param {string} [options.passphrase] SSL Certificate pass phrase
|
||||
* @param {boolean} [options.rejectUnauthorized=true] Reject unauthorized server certificates
|
||||
* @param {boolean} [options.promoteLongs=true] Convert Long values from the db into Numbers if they fit into 53 bits
|
||||
* @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.
|
||||
* @fires Connection#connect
|
||||
* @fires Connection#close
|
||||
* @fires Connection#error
|
||||
* @fires Connection#timeout
|
||||
* @fires Connection#parseError
|
||||
* @return {Connection} A cursor instance
|
||||
*/
|
||||
var Connection = function(messageHandler, options) {
|
||||
// Add event listener
|
||||
EventEmitter.call(this);
|
||||
// Set empty if no options passed
|
||||
this.options = options || {};
|
||||
// Identification information
|
||||
this.id = _id++;
|
||||
// Logger instance
|
||||
this.logger = Logger('Connection', options);
|
||||
// No bson parser passed in
|
||||
if (!options.bson) throw new Error('must pass in valid bson parser');
|
||||
// Get bson parser
|
||||
this.bson = options.bson;
|
||||
// Grouping tag used for debugging purposes
|
||||
this.tag = options.tag;
|
||||
// Message handler
|
||||
this.messageHandler = messageHandler;
|
||||
|
||||
// Max BSON message size
|
||||
this.maxBsonMessageSize = options.maxBsonMessageSize || 1024 * 1024 * 16 * 4;
|
||||
// Debug information
|
||||
if (this.logger.isDebug())
|
||||
this.logger.debug(
|
||||
f(
|
||||
'creating connection %s with options [%s]',
|
||||
this.id,
|
||||
JSON.stringify(debugOptions(debugFields, options))
|
||||
)
|
||||
);
|
||||
|
||||
// Default options
|
||||
this.port = options.port || 27017;
|
||||
this.host = options.host || 'localhost';
|
||||
this.family = typeof options.family === 'number' ? options.family : void 0;
|
||||
this.keepAlive = typeof options.keepAlive === 'boolean' ? options.keepAlive : true;
|
||||
this.keepAliveInitialDelay =
|
||||
typeof options.keepAliveInitialDelay === 'number' ? options.keepAliveInitialDelay : 300000;
|
||||
this.noDelay = typeof options.noDelay === 'boolean' ? options.noDelay : true;
|
||||
this.connectionTimeout =
|
||||
typeof options.connectionTimeout === 'number' ? options.connectionTimeout : 30000;
|
||||
this.socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
|
||||
|
||||
// Is the keepAliveInitialDelay > socketTimeout set it to half of socketTimeout
|
||||
if (this.keepAliveInitialDelay > this.socketTimeout) {
|
||||
this.keepAliveInitialDelay = Math.round(this.socketTimeout / 2);
|
||||
}
|
||||
|
||||
// If connection was destroyed
|
||||
this.destroyed = false;
|
||||
|
||||
// Check if we have a domain socket
|
||||
this.domainSocket = this.host.indexOf('/') !== -1;
|
||||
|
||||
// Serialize commands using function
|
||||
this.singleBufferSerializtion =
|
||||
typeof options.singleBufferSerializtion === 'boolean' ? options.singleBufferSerializtion : true;
|
||||
this.serializationFunction = this.singleBufferSerializtion ? 'toBinUnified' : 'toBin';
|
||||
|
||||
// SSL options
|
||||
this.ca = options.ca || null;
|
||||
this.crl = options.crl || null;
|
||||
this.cert = options.cert || null;
|
||||
this.key = options.key || null;
|
||||
this.passphrase = options.passphrase || null;
|
||||
this.ciphers = options.ciphers || null;
|
||||
this.ecdhCurve = options.ecdhCurve || null;
|
||||
this.ssl = typeof options.ssl === 'boolean' ? options.ssl : false;
|
||||
this.rejectUnauthorized =
|
||||
typeof options.rejectUnauthorized === 'boolean' ? options.rejectUnauthorized : true;
|
||||
this.checkServerIdentity =
|
||||
typeof options.checkServerIdentity === 'boolean' ||
|
||||
typeof options.checkServerIdentity === 'function'
|
||||
? options.checkServerIdentity
|
||||
: true;
|
||||
|
||||
// If ssl not enabled
|
||||
if (!this.ssl) this.rejectUnauthorized = false;
|
||||
|
||||
// Response options
|
||||
this.responseOptions = {
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false
|
||||
};
|
||||
|
||||
// Flushing
|
||||
this.flushing = false;
|
||||
this.queue = [];
|
||||
|
||||
// Internal state
|
||||
this.connection = null;
|
||||
this.writeStream = null;
|
||||
|
||||
// Create hash method
|
||||
var hash = crypto.createHash('sha1');
|
||||
hash.update(f('%s:%s', this.host, this.port));
|
||||
|
||||
// Create a hash name
|
||||
this.hashedName = hash.digest('hex');
|
||||
|
||||
// All operations in flight on the connection
|
||||
this.workItems = [];
|
||||
};
|
||||
|
||||
inherits(Connection, EventEmitter);
|
||||
|
||||
Connection.prototype.setSocketTimeout = function(value) {
|
||||
if (this.connection) {
|
||||
this.connection.setTimeout(value);
|
||||
}
|
||||
};
|
||||
|
||||
Connection.prototype.resetSocketTimeout = function() {
|
||||
if (this.connection) {
|
||||
this.connection.setTimeout(this.socketTimeout);
|
||||
}
|
||||
};
|
||||
|
||||
Connection.enableConnectionAccounting = function(spy) {
|
||||
if (spy) {
|
||||
connectionAccountingSpy = spy;
|
||||
}
|
||||
|
||||
connectionAccounting = true;
|
||||
connections = {};
|
||||
};
|
||||
|
||||
Connection.disableConnectionAccounting = function() {
|
||||
connectionAccounting = false;
|
||||
connectionAccountingSpy = undefined;
|
||||
};
|
||||
|
||||
Connection.connections = function() {
|
||||
return connections;
|
||||
};
|
||||
|
||||
function deleteConnection(id) {
|
||||
// console.log("=== deleted connection " + id + " :: " + (connections[id] ? connections[id].port : ''))
|
||||
delete connections[id];
|
||||
|
||||
if (connectionAccountingSpy) {
|
||||
connectionAccountingSpy.deleteConnection(id);
|
||||
}
|
||||
}
|
||||
|
||||
function addConnection(id, connection) {
|
||||
// console.log("=== added connection " + id + " :: " + connection.port)
|
||||
connections[id] = connection;
|
||||
|
||||
if (connectionAccountingSpy) {
|
||||
connectionAccountingSpy.addConnection(id, connection);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Connection handlers
|
||||
var errorHandler = function(self) {
|
||||
return function(err) {
|
||||
if (connectionAccounting) deleteConnection(self.id);
|
||||
// Debug information
|
||||
if (self.logger.isDebug())
|
||||
self.logger.debug(
|
||||
f(
|
||||
'connection %s for [%s:%s] errored out with [%s]',
|
||||
self.id,
|
||||
self.host,
|
||||
self.port,
|
||||
JSON.stringify(err)
|
||||
)
|
||||
);
|
||||
// Emit the error
|
||||
if (self.listeners('error').length > 0) self.emit('error', new MongoNetworkError(err), self);
|
||||
};
|
||||
};
|
||||
|
||||
var timeoutHandler = function(self) {
|
||||
return function() {
|
||||
if (connectionAccounting) deleteConnection(self.id);
|
||||
// Debug information
|
||||
if (self.logger.isDebug())
|
||||
self.logger.debug(f('connection %s for [%s:%s] timed out', self.id, self.host, self.port));
|
||||
// Emit timeout error
|
||||
self.emit(
|
||||
'timeout',
|
||||
new MongoNetworkError(f('connection %s to %s:%s timed out', self.id, self.host, self.port)),
|
||||
self
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
var closeHandler = function(self) {
|
||||
return function(hadError) {
|
||||
if (connectionAccounting) deleteConnection(self.id);
|
||||
// Debug information
|
||||
if (self.logger.isDebug())
|
||||
self.logger.debug(f('connection %s with for [%s:%s] closed', self.id, self.host, self.port));
|
||||
|
||||
// Emit close event
|
||||
if (!hadError) {
|
||||
self.emit(
|
||||
'close',
|
||||
new MongoNetworkError(f('connection %s to %s:%s closed', self.id, self.host, self.port)),
|
||||
self
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Handle a message once it is received
|
||||
var emitMessageHandler = function(self, message) {
|
||||
var msgHeader = parseHeader(message);
|
||||
if (msgHeader.opCode === OP_COMPRESSED) {
|
||||
msgHeader.fromCompressed = true;
|
||||
var index = MESSAGE_HEADER_SIZE;
|
||||
msgHeader.opCode = message.readInt32LE(index);
|
||||
index += 4;
|
||||
msgHeader.length = message.readInt32LE(index);
|
||||
index += 4;
|
||||
var compressorID = message[index];
|
||||
index++;
|
||||
decompress(compressorID, message.slice(index), function(err, decompressedMsgBody) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (decompressedMsgBody.length !== msgHeader.length) {
|
||||
throw new Error(
|
||||
'Decompressing a compressed message from the server failed. The message is corrupt.'
|
||||
);
|
||||
}
|
||||
self.messageHandler(
|
||||
new Response(self.bson, message, msgHeader, decompressedMsgBody, self.responseOptions),
|
||||
self
|
||||
);
|
||||
});
|
||||
} else {
|
||||
self.messageHandler(
|
||||
new Response(
|
||||
self.bson,
|
||||
message,
|
||||
msgHeader,
|
||||
message.slice(MESSAGE_HEADER_SIZE),
|
||||
self.responseOptions
|
||||
),
|
||||
self
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
var dataHandler = function(self) {
|
||||
return function(data) {
|
||||
// Parse until we are done with the data
|
||||
while (data.length > 0) {
|
||||
// If we still have bytes to read on the current message
|
||||
if (self.bytesRead > 0 && self.sizeOfMessage > 0) {
|
||||
// Calculate the amount of remaining bytes
|
||||
var remainingBytesToRead = self.sizeOfMessage - self.bytesRead;
|
||||
// Check if the current chunk contains the rest of the message
|
||||
if (remainingBytesToRead > data.length) {
|
||||
// Copy the new data into the exiting buffer (should have been allocated when we know the message size)
|
||||
data.copy(self.buffer, self.bytesRead);
|
||||
// Adjust the number of bytes read so it point to the correct index in the buffer
|
||||
self.bytesRead = self.bytesRead + data.length;
|
||||
|
||||
// Reset state of buffer
|
||||
data = Buffer.alloc(0);
|
||||
} else {
|
||||
// Copy the missing part of the data into our current buffer
|
||||
data.copy(self.buffer, self.bytesRead, 0, remainingBytesToRead);
|
||||
// Slice the overflow into a new buffer that we will then re-parse
|
||||
data = data.slice(remainingBytesToRead);
|
||||
|
||||
// Emit current complete message
|
||||
try {
|
||||
var emitBuffer = self.buffer;
|
||||
// Reset state of buffer
|
||||
self.buffer = null;
|
||||
self.sizeOfMessage = 0;
|
||||
self.bytesRead = 0;
|
||||
self.stubBuffer = null;
|
||||
|
||||
emitMessageHandler(self, emitBuffer);
|
||||
} catch (err) {
|
||||
var errorObject = {
|
||||
err: 'socketHandler',
|
||||
trace: err,
|
||||
bin: self.buffer,
|
||||
parseState: {
|
||||
sizeOfMessage: self.sizeOfMessage,
|
||||
bytesRead: self.bytesRead,
|
||||
stubBuffer: self.stubBuffer
|
||||
}
|
||||
};
|
||||
// We got a parse Error fire it off then keep going
|
||||
self.emit('parseError', errorObject, self);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stub buffer is kept in case we don't get enough bytes to determine the
|
||||
// size of the message (< 4 bytes)
|
||||
if (self.stubBuffer != null && self.stubBuffer.length > 0) {
|
||||
// If we have enough bytes to determine the message size let's do it
|
||||
if (self.stubBuffer.length + data.length > 4) {
|
||||
// Prepad the data
|
||||
var newData = Buffer.alloc(self.stubBuffer.length + data.length);
|
||||
self.stubBuffer.copy(newData, 0);
|
||||
data.copy(newData, self.stubBuffer.length);
|
||||
// Reassign for parsing
|
||||
data = newData;
|
||||
|
||||
// Reset state of buffer
|
||||
self.buffer = null;
|
||||
self.sizeOfMessage = 0;
|
||||
self.bytesRead = 0;
|
||||
self.stubBuffer = null;
|
||||
} else {
|
||||
// Add the the bytes to the stub buffer
|
||||
var newStubBuffer = Buffer.alloc(self.stubBuffer.length + data.length);
|
||||
// Copy existing stub buffer
|
||||
self.stubBuffer.copy(newStubBuffer, 0);
|
||||
// Copy missing part of the data
|
||||
data.copy(newStubBuffer, self.stubBuffer.length);
|
||||
// Exit parsing loop
|
||||
data = Buffer.alloc(0);
|
||||
}
|
||||
} else {
|
||||
if (data.length > 4) {
|
||||
// Retrieve the message size
|
||||
// var sizeOfMessage = data.readUInt32LE(0);
|
||||
var sizeOfMessage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||
// If we have a negative sizeOfMessage emit error and return
|
||||
if (sizeOfMessage < 0 || sizeOfMessage > self.maxBsonMessageSize) {
|
||||
errorObject = {
|
||||
err: 'socketHandler',
|
||||
trace: '',
|
||||
bin: self.buffer,
|
||||
parseState: {
|
||||
sizeOfMessage: sizeOfMessage,
|
||||
bytesRead: self.bytesRead,
|
||||
stubBuffer: self.stubBuffer
|
||||
}
|
||||
};
|
||||
// We got a parse Error fire it off then keep going
|
||||
self.emit('parseError', errorObject, self);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the size of message is larger than 0 and less than the max allowed
|
||||
if (
|
||||
sizeOfMessage > 4 &&
|
||||
sizeOfMessage < self.maxBsonMessageSize &&
|
||||
sizeOfMessage > data.length
|
||||
) {
|
||||
self.buffer = Buffer.alloc(sizeOfMessage);
|
||||
// Copy all the data into the buffer
|
||||
data.copy(self.buffer, 0);
|
||||
// Update bytes read
|
||||
self.bytesRead = data.length;
|
||||
// Update sizeOfMessage
|
||||
self.sizeOfMessage = sizeOfMessage;
|
||||
// Ensure stub buffer is null
|
||||
self.stubBuffer = null;
|
||||
// Exit parsing loop
|
||||
data = Buffer.alloc(0);
|
||||
} else if (
|
||||
sizeOfMessage > 4 &&
|
||||
sizeOfMessage < self.maxBsonMessageSize &&
|
||||
sizeOfMessage === data.length
|
||||
) {
|
||||
try {
|
||||
emitBuffer = data;
|
||||
// Reset state of buffer
|
||||
self.buffer = null;
|
||||
self.sizeOfMessage = 0;
|
||||
self.bytesRead = 0;
|
||||
self.stubBuffer = null;
|
||||
// Exit parsing loop
|
||||
data = Buffer.alloc(0);
|
||||
// Emit the message
|
||||
emitMessageHandler(self, emitBuffer);
|
||||
} catch (err) {
|
||||
self.emit('parseError', err, self);
|
||||
}
|
||||
} else if (sizeOfMessage <= 4 || sizeOfMessage > self.maxBsonMessageSize) {
|
||||
errorObject = {
|
||||
err: 'socketHandler',
|
||||
trace: null,
|
||||
bin: data,
|
||||
parseState: {
|
||||
sizeOfMessage: sizeOfMessage,
|
||||
bytesRead: 0,
|
||||
buffer: null,
|
||||
stubBuffer: null
|
||||
}
|
||||
};
|
||||
// We got a parse Error fire it off then keep going
|
||||
self.emit('parseError', errorObject, self);
|
||||
|
||||
// Clear out the state of the parser
|
||||
self.buffer = null;
|
||||
self.sizeOfMessage = 0;
|
||||
self.bytesRead = 0;
|
||||
self.stubBuffer = null;
|
||||
// Exit parsing loop
|
||||
data = Buffer.alloc(0);
|
||||
} else {
|
||||
emitBuffer = data.slice(0, sizeOfMessage);
|
||||
// Reset state of buffer
|
||||
self.buffer = null;
|
||||
self.sizeOfMessage = 0;
|
||||
self.bytesRead = 0;
|
||||
self.stubBuffer = null;
|
||||
// Copy rest of message
|
||||
data = data.slice(sizeOfMessage);
|
||||
// Emit the message
|
||||
emitMessageHandler(self, emitBuffer);
|
||||
}
|
||||
} else {
|
||||
// Create a buffer that contains the space for the non-complete message
|
||||
self.stubBuffer = Buffer.alloc(data.length);
|
||||
// Copy the data to the stub buffer
|
||||
data.copy(self.stubBuffer, 0);
|
||||
// Exit parsing loop
|
||||
data = Buffer.alloc(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// List of socket level valid ssl options
|
||||
var legalSslSocketOptions = [
|
||||
'pfx',
|
||||
'key',
|
||||
'passphrase',
|
||||
'cert',
|
||||
'ca',
|
||||
'ciphers',
|
||||
'NPNProtocols',
|
||||
'ALPNProtocols',
|
||||
'servername',
|
||||
'ecdhCurve',
|
||||
'secureProtocol',
|
||||
'secureContext',
|
||||
'session',
|
||||
'minDHSize'
|
||||
];
|
||||
|
||||
function merge(options1, options2) {
|
||||
// Merge in any allowed ssl options
|
||||
for (var name in options2) {
|
||||
if (options2[name] != null && legalSslSocketOptions.indexOf(name) !== -1) {
|
||||
options1[name] = options2[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeSSLConnection(self, _options) {
|
||||
let sslOptions = {
|
||||
socket: self.connection,
|
||||
rejectUnauthorized: self.rejectUnauthorized
|
||||
};
|
||||
|
||||
// Merge in options
|
||||
merge(sslOptions, self.options);
|
||||
merge(sslOptions, _options);
|
||||
|
||||
// Set options for ssl
|
||||
if (self.ca) sslOptions.ca = self.ca;
|
||||
if (self.crl) sslOptions.crl = self.crl;
|
||||
if (self.cert) sslOptions.cert = self.cert;
|
||||
if (self.key) sslOptions.key = self.key;
|
||||
if (self.passphrase) sslOptions.passphrase = self.passphrase;
|
||||
|
||||
// Override checkServerIdentity behavior
|
||||
if (self.checkServerIdentity === false) {
|
||||
// Skip the identiy check by retuning undefined as per node documents
|
||||
// https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
|
||||
sslOptions.checkServerIdentity = function() {
|
||||
return undefined;
|
||||
};
|
||||
} else if (typeof self.checkServerIdentity === 'function') {
|
||||
sslOptions.checkServerIdentity = self.checkServerIdentity;
|
||||
}
|
||||
|
||||
// Set default sni servername to be the same as host
|
||||
if (sslOptions.servername == null) {
|
||||
sslOptions.servername = self.host;
|
||||
}
|
||||
|
||||
// Attempt SSL connection
|
||||
const connection = tls.connect(self.port, self.host, sslOptions, function() {
|
||||
// Error on auth or skip
|
||||
if (connection.authorizationError && self.rejectUnauthorized) {
|
||||
return self.emit('error', connection.authorizationError, self, { ssl: true });
|
||||
}
|
||||
|
||||
// Set socket timeout instead of connection timeout
|
||||
connection.setTimeout(self.socketTimeout);
|
||||
// We are done emit connect
|
||||
self.emit('connect', self);
|
||||
});
|
||||
|
||||
// Set the options for the connection
|
||||
connection.setKeepAlive(self.keepAlive, self.keepAliveInitialDelay);
|
||||
connection.setTimeout(self.connectionTimeout);
|
||||
connection.setNoDelay(self.noDelay);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
function makeUnsecureConnection(self, family) {
|
||||
// Create new connection instance
|
||||
let connection_options;
|
||||
if (self.domainSocket) {
|
||||
connection_options = { path: self.host };
|
||||
} else {
|
||||
connection_options = { port: self.port, host: self.host };
|
||||
connection_options.family = family;
|
||||
}
|
||||
|
||||
const connection = net.createConnection(connection_options);
|
||||
|
||||
// Set the options for the connection
|
||||
connection.setKeepAlive(self.keepAlive, self.keepAliveInitialDelay);
|
||||
connection.setTimeout(self.connectionTimeout);
|
||||
connection.setNoDelay(self.noDelay);
|
||||
|
||||
connection.once('connect', function() {
|
||||
// Set socket timeout instead of connection timeout
|
||||
connection.setTimeout(self.socketTimeout);
|
||||
// Emit connect event
|
||||
self.emit('connect', self);
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
function doConnect(self, family, _options, _errorHandler) {
|
||||
self.connection = self.ssl
|
||||
? makeSSLConnection(self, _options)
|
||||
: makeUnsecureConnection(self, family);
|
||||
|
||||
// Add handlers for events
|
||||
self.connection.once('error', _errorHandler);
|
||||
self.connection.once('timeout', timeoutHandler(self));
|
||||
self.connection.once('close', closeHandler(self));
|
||||
self.connection.on('data', dataHandler(self));
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect
|
||||
* @method
|
||||
*/
|
||||
Connection.prototype.connect = function(_options) {
|
||||
_options = _options || {};
|
||||
// Set the connections
|
||||
if (connectionAccounting) addConnection(this.id, this);
|
||||
// Check if we are overriding the promoteLongs
|
||||
if (typeof _options.promoteLongs === 'boolean') {
|
||||
this.responseOptions.promoteLongs = _options.promoteLongs;
|
||||
this.responseOptions.promoteValues = _options.promoteValues;
|
||||
this.responseOptions.promoteBuffers = _options.promoteBuffers;
|
||||
}
|
||||
|
||||
const _errorHandler = errorHandler(this);
|
||||
|
||||
if (this.family !== void 0) {
|
||||
return doConnect(this, this.family, _options, _errorHandler);
|
||||
}
|
||||
|
||||
return doConnect(this, 6, _options, err => {
|
||||
if (this.logger.isDebug()) {
|
||||
this.logger.debug(
|
||||
f(
|
||||
'connection %s for [%s:%s] errored out with [%s]',
|
||||
this.id,
|
||||
this.host,
|
||||
this.port,
|
||||
JSON.stringify(err)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// clean up existing event handlers
|
||||
this.connection.removeAllListeners('error');
|
||||
this.connection.removeAllListeners('timeout');
|
||||
this.connection.removeAllListeners('close');
|
||||
this.connection.removeAllListeners('data');
|
||||
this.connection = undefined;
|
||||
|
||||
return doConnect(this, 4, _options, _errorHandler);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Unref this connection
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
Connection.prototype.unref = function() {
|
||||
if (this.connection) this.connection.unref();
|
||||
else {
|
||||
var self = this;
|
||||
this.once('connect', function() {
|
||||
self.connection.unref();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy connection
|
||||
* @method
|
||||
*/
|
||||
Connection.prototype.destroy = function() {
|
||||
// Set the connections
|
||||
if (connectionAccounting) deleteConnection(this.id);
|
||||
if (this.connection) {
|
||||
// Catch posssible exception thrown by node 0.10.x
|
||||
try {
|
||||
this.connection.end();
|
||||
} catch (err) {} // eslint-disable-line
|
||||
// Destroy connection
|
||||
this.connection.destroy();
|
||||
}
|
||||
|
||||
this.destroyed = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Write to connection
|
||||
* @method
|
||||
* @param {Command} command Command to write out need to implement toBin and toBinUnified
|
||||
*/
|
||||
Connection.prototype.write = function(buffer) {
|
||||
var i;
|
||||
// Debug Log
|
||||
if (this.logger.isDebug()) {
|
||||
if (!Array.isArray(buffer)) {
|
||||
this.logger.debug(
|
||||
f('writing buffer [%s] to %s:%s', buffer.toString('hex'), this.host, this.port)
|
||||
);
|
||||
} else {
|
||||
for (i = 0; i < buffer.length; i++)
|
||||
this.logger.debug(
|
||||
f('writing buffer [%s] to %s:%s', buffer[i].toString('hex'), this.host, this.port)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Double check that the connection is not destroyed
|
||||
if (this.connection.destroyed === false) {
|
||||
// Write out the command
|
||||
if (!Array.isArray(buffer)) {
|
||||
this.connection.write(buffer, 'binary');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Iterate over all buffers and write them in order to the socket
|
||||
for (i = 0; i < buffer.length; i++) this.connection.write(buffer[i], 'binary');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Connection is destroyed return write failed
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return id of connection as a string
|
||||
* @method
|
||||
* @return {string}
|
||||
*/
|
||||
Connection.prototype.toString = function() {
|
||||
return '' + this.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return json object of connection
|
||||
* @method
|
||||
* @return {object}
|
||||
*/
|
||||
Connection.prototype.toJSON = function() {
|
||||
return { id: this.id, host: this.host, port: this.port };
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the connection connected
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
Connection.prototype.isConnected = function() {
|
||||
if (this.destroyed) return false;
|
||||
return !this.connection.destroyed && this.connection.writable;
|
||||
};
|
||||
|
||||
/**
|
||||
* A server connect event, used to verify that the connection is up and running
|
||||
*
|
||||
* @event Connection#connect
|
||||
* @type {Connection}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The server connection closed, all pool connections closed
|
||||
*
|
||||
* @event Connection#close
|
||||
* @type {Connection}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The server connection caused an error, all pool connections closed
|
||||
*
|
||||
* @event Connection#error
|
||||
* @type {Connection}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The server connection timed out, all pool connections closed
|
||||
*
|
||||
* @event Connection#timeout
|
||||
* @type {Connection}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The driver experienced an invalid message, all pool connections closed
|
||||
*
|
||||
* @event Connection#parseError
|
||||
* @type {Connection}
|
||||
*/
|
||||
|
||||
module.exports = Connection;
|
246
node_modules/mongodb-core/lib/connection/logger.js
generated
vendored
Normal file
246
node_modules/mongodb-core/lib/connection/logger.js
generated
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
// Filters for classes
|
||||
var classFilters = {};
|
||||
var filteredClasses = {};
|
||||
var level = null;
|
||||
// Save the process id
|
||||
var pid = process.pid;
|
||||
// current logger
|
||||
var currentLogger = null;
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance
|
||||
* @class
|
||||
* @param {string} className The Class name associated with the logging instance
|
||||
* @param {object} [options=null] Optional settings.
|
||||
* @param {Function} [options.logger=null] Custom logger function;
|
||||
* @param {string} [options.loggerLevel=error] Override default global log level.
|
||||
* @return {Logger} a Logger instance.
|
||||
*/
|
||||
var Logger = function(className, options) {
|
||||
if (!(this instanceof Logger)) return new Logger(className, options);
|
||||
options = options || {};
|
||||
|
||||
// Current reference
|
||||
this.className = className;
|
||||
|
||||
// Current logger
|
||||
if (options.logger) {
|
||||
currentLogger = options.logger;
|
||||
} else if (currentLogger == null) {
|
||||
currentLogger = console.log;
|
||||
}
|
||||
|
||||
// Set level of logging, default is error
|
||||
if (options.loggerLevel) {
|
||||
level = options.loggerLevel || 'error';
|
||||
}
|
||||
|
||||
// Add all class names
|
||||
if (filteredClasses[this.className] == null) classFilters[this.className] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a message at the debug level
|
||||
* @method
|
||||
* @param {string} message The message to log
|
||||
* @param {object} object additional meta data to log
|
||||
* @return {null}
|
||||
*/
|
||||
Logger.prototype.debug = function(message, object) {
|
||||
if (
|
||||
this.isDebug() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
var dateTime = new Date().getTime();
|
||||
var msg = f('[%s-%s:%s] %s %s', 'DEBUG', this.className, pid, dateTime, message);
|
||||
var state = {
|
||||
type: 'debug',
|
||||
message: message,
|
||||
className: this.className,
|
||||
pid: pid,
|
||||
date: dateTime
|
||||
};
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a message at the warn level
|
||||
* @method
|
||||
* @param {string} message The message to log
|
||||
* @param {object} object additional meta data to log
|
||||
* @return {null}
|
||||
*/
|
||||
(Logger.prototype.warn = function(message, object) {
|
||||
if (
|
||||
this.isWarn() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
var dateTime = new Date().getTime();
|
||||
var msg = f('[%s-%s:%s] %s %s', 'WARN', this.className, pid, dateTime, message);
|
||||
var state = {
|
||||
type: 'warn',
|
||||
message: message,
|
||||
className: this.className,
|
||||
pid: pid,
|
||||
date: dateTime
|
||||
};
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}),
|
||||
/**
|
||||
* Log a message at the info level
|
||||
* @method
|
||||
* @param {string} message The message to log
|
||||
* @param {object} object additional meta data to log
|
||||
* @return {null}
|
||||
*/
|
||||
(Logger.prototype.info = function(message, object) {
|
||||
if (
|
||||
this.isInfo() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
var dateTime = new Date().getTime();
|
||||
var msg = f('[%s-%s:%s] %s %s', 'INFO', this.className, pid, dateTime, message);
|
||||
var state = {
|
||||
type: 'info',
|
||||
message: message,
|
||||
className: this.className,
|
||||
pid: pid,
|
||||
date: dateTime
|
||||
};
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}),
|
||||
/**
|
||||
* Log a message at the error level
|
||||
* @method
|
||||
* @param {string} message The message to log
|
||||
* @param {object} object additional meta data to log
|
||||
* @return {null}
|
||||
*/
|
||||
(Logger.prototype.error = function(message, object) {
|
||||
if (
|
||||
this.isError() &&
|
||||
((Object.keys(filteredClasses).length > 0 && filteredClasses[this.className]) ||
|
||||
(Object.keys(filteredClasses).length === 0 && classFilters[this.className]))
|
||||
) {
|
||||
var dateTime = new Date().getTime();
|
||||
var msg = f('[%s-%s:%s] %s %s', 'ERROR', this.className, pid, dateTime, message);
|
||||
var state = {
|
||||
type: 'error',
|
||||
message: message,
|
||||
className: this.className,
|
||||
pid: pid,
|
||||
date: dateTime
|
||||
};
|
||||
if (object) state.meta = object;
|
||||
currentLogger(msg, state);
|
||||
}
|
||||
}),
|
||||
/**
|
||||
* Is the logger set at info level
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
(Logger.prototype.isInfo = function() {
|
||||
return level === 'info' || level === 'debug';
|
||||
}),
|
||||
/**
|
||||
* Is the logger set at error level
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
(Logger.prototype.isError = function() {
|
||||
return level === 'error' || level === 'info' || level === 'debug';
|
||||
}),
|
||||
/**
|
||||
* Is the logger set at error level
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
(Logger.prototype.isWarn = function() {
|
||||
return level === 'error' || level === 'warn' || level === 'info' || level === 'debug';
|
||||
}),
|
||||
/**
|
||||
* Is the logger set at debug level
|
||||
* @method
|
||||
* @return {boolean}
|
||||
*/
|
||||
(Logger.prototype.isDebug = function() {
|
||||
return level === 'debug';
|
||||
});
|
||||
|
||||
/**
|
||||
* Resets the logger to default settings, error and no filtered classes
|
||||
* @method
|
||||
* @return {null}
|
||||
*/
|
||||
Logger.reset = function() {
|
||||
level = 'error';
|
||||
filteredClasses = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current logger function
|
||||
* @method
|
||||
* @return {function}
|
||||
*/
|
||||
Logger.currentLogger = function() {
|
||||
return currentLogger;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current logger function
|
||||
* @method
|
||||
* @param {function} logger Logger function.
|
||||
* @return {null}
|
||||
*/
|
||||
Logger.setCurrentLogger = function(logger) {
|
||||
if (typeof logger !== 'function') throw new MongoError('current logger must be a function');
|
||||
currentLogger = logger;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set what classes to log.
|
||||
* @method
|
||||
* @param {string} type The type of filter (currently only class)
|
||||
* @param {string[]} values The filters to apply
|
||||
* @return {null}
|
||||
*/
|
||||
Logger.filter = function(type, values) {
|
||||
if (type === 'class' && Array.isArray(values)) {
|
||||
filteredClasses = {};
|
||||
|
||||
values.forEach(function(x) {
|
||||
filteredClasses[x] = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current log level
|
||||
* @method
|
||||
* @param {string} level Set current log level (debug, info, error)
|
||||
* @return {null}
|
||||
*/
|
||||
Logger.setLevel = function(_level) {
|
||||
if (_level !== 'info' && _level !== 'error' && _level !== 'debug' && _level !== 'warn') {
|
||||
throw new Error(f('%s is an illegal logging level', _level));
|
||||
}
|
||||
|
||||
level = _level;
|
||||
};
|
||||
|
||||
module.exports = Logger;
|
1657
node_modules/mongodb-core/lib/connection/pool.js
generated
vendored
Normal file
1657
node_modules/mongodb-core/lib/connection/pool.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
113
node_modules/mongodb-core/lib/connection/utils.js
generated
vendored
Normal file
113
node_modules/mongodb-core/lib/connection/utils.js
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
var f = require('util').format,
|
||||
require_optional = require('require_optional');
|
||||
|
||||
// Set property function
|
||||
var setProperty = function(obj, prop, flag, values) {
|
||||
Object.defineProperty(obj, prop.name, {
|
||||
enumerable: true,
|
||||
set: function(value) {
|
||||
if (typeof value !== 'boolean') throw new Error(f('%s required a boolean', prop.name));
|
||||
// Flip the bit to 1
|
||||
if (value === true) values.flags |= flag;
|
||||
// Flip the bit to 0 if it's set, otherwise ignore
|
||||
if (value === false && (values.flags & flag) === flag) values.flags ^= flag;
|
||||
prop.value = value;
|
||||
},
|
||||
get: function() {
|
||||
return prop.value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set property function
|
||||
var getProperty = function(obj, propName, fieldName, values, func) {
|
||||
Object.defineProperty(obj, propName, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
// Not parsed yet, parse it
|
||||
if (values[fieldName] == null && obj.isParsed && !obj.isParsed()) {
|
||||
obj.parse();
|
||||
}
|
||||
|
||||
// Do we have a post processing function
|
||||
if (typeof func === 'function') return func(values[fieldName]);
|
||||
// Return raw value
|
||||
return values[fieldName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set simple property
|
||||
var getSingleProperty = function(obj, name, value) {
|
||||
Object.defineProperty(obj, name, {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Shallow copy
|
||||
var copy = function(fObj, tObj) {
|
||||
tObj = tObj || {};
|
||||
for (var name in fObj) tObj[name] = fObj[name];
|
||||
return tObj;
|
||||
};
|
||||
|
||||
var debugOptions = function(debugFields, options) {
|
||||
var finaloptions = {};
|
||||
debugFields.forEach(function(n) {
|
||||
finaloptions[n] = options[n];
|
||||
});
|
||||
|
||||
return finaloptions;
|
||||
};
|
||||
|
||||
var retrieveBSON = function() {
|
||||
var BSON = require('bson');
|
||||
BSON.native = false;
|
||||
|
||||
try {
|
||||
var optionalBSON = require_optional('bson-ext');
|
||||
if (optionalBSON) {
|
||||
optionalBSON.native = true;
|
||||
return optionalBSON;
|
||||
}
|
||||
} catch (err) {} // eslint-disable-line
|
||||
|
||||
return BSON;
|
||||
};
|
||||
|
||||
// Throw an error if an attempt to use Snappy is made when Snappy is not installed
|
||||
var noSnappyWarning = function() {
|
||||
throw new Error(
|
||||
'Attempted to use Snappy compression, but Snappy is not installed. Install or disable Snappy compression and try again.'
|
||||
);
|
||||
};
|
||||
|
||||
// Facilitate loading Snappy optionally
|
||||
var retrieveSnappy = function() {
|
||||
var snappy = null;
|
||||
try {
|
||||
snappy = require_optional('snappy');
|
||||
} catch (error) {} // eslint-disable-line
|
||||
if (!snappy) {
|
||||
snappy = {
|
||||
compress: noSnappyWarning,
|
||||
uncompress: noSnappyWarning,
|
||||
compressSync: noSnappyWarning,
|
||||
uncompressSync: noSnappyWarning
|
||||
};
|
||||
}
|
||||
return snappy;
|
||||
};
|
||||
|
||||
exports.setProperty = setProperty;
|
||||
exports.getProperty = getProperty;
|
||||
exports.getSingleProperty = getSingleProperty;
|
||||
exports.copy = copy;
|
||||
exports.debugOptions = debugOptions;
|
||||
exports.retrieveBSON = retrieveBSON;
|
||||
exports.retrieveSnappy = retrieveSnappy;
|
766
node_modules/mongodb-core/lib/cursor.js
generated
vendored
Normal file
766
node_modules/mongodb-core/lib/cursor.js
generated
vendored
Normal file
@@ -0,0 +1,766 @@
|
||||
'use strict';
|
||||
|
||||
const Logger = require('./connection/logger');
|
||||
const retrieveBSON = require('./connection/utils').retrieveBSON;
|
||||
const MongoError = require('./error').MongoError;
|
||||
const MongoNetworkError = require('./error').MongoNetworkError;
|
||||
const mongoErrorContextSymbol = require('./error').mongoErrorContextSymbol;
|
||||
const f = require('util').format;
|
||||
const collationNotSupported = require('./utils').collationNotSupported;
|
||||
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
|
||||
/**
|
||||
* This is a cursor results callback
|
||||
*
|
||||
* @callback resultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {object} document
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview The **Cursor** class is an internal class that embodies a cursor on MongoDB
|
||||
* allowing for iteration over the results returned from the underlying query.
|
||||
*
|
||||
* **CURSORS Cannot directly be instantiated**
|
||||
* @example
|
||||
* var Server = require('mongodb-core').Server
|
||||
* , ReadPreference = require('mongodb-core').ReadPreference
|
||||
* , assert = require('assert');
|
||||
*
|
||||
* var server = new Server({host: 'localhost', port: 27017});
|
||||
* // Wait for the connection event
|
||||
* server.on('connect', function(server) {
|
||||
* assert.equal(null, err);
|
||||
*
|
||||
* // Execute the write
|
||||
* var cursor = _server.cursor('integration_tests.inserts_example4', {
|
||||
* find: 'integration_tests.example4'
|
||||
* , query: {a:1}
|
||||
* }, {
|
||||
* readPreference: new ReadPreference('secondary');
|
||||
* });
|
||||
*
|
||||
* // Get the first document
|
||||
* cursor.next(function(err, doc) {
|
||||
* assert.equal(null, err);
|
||||
* server.destroy();
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* // Start connecting
|
||||
* server.connect();
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new Cursor, not to be used directly
|
||||
* @class
|
||||
* @param {object} bson An instance of the BSON parser
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {{object}|Long} cmd The selector (can be a command or a cursorId)
|
||||
* @param {object} [options=null] Optional settings.
|
||||
* @param {object} [options.batchSize=1000] Batchsize for the operation
|
||||
* @param {array} [options.documents=[]] Initial documents list for cursor
|
||||
* @param {object} [options.transforms=null] Transform methods for the cursor results
|
||||
* @param {function} [options.transforms.query] Transform the value returned from the initial query
|
||||
* @param {function} [options.transforms.doc] Transform each document returned from Cursor.prototype.next
|
||||
* @param {object} topology The server topology instance.
|
||||
* @param {object} topologyOptions The server topology options.
|
||||
* @return {Cursor} A cursor instance
|
||||
* @property {number} cursorBatchSize The current cursorBatchSize for the cursor
|
||||
* @property {number} cursorLimit The current cursorLimit for the cursor
|
||||
* @property {number} cursorSkip The current cursorSkip for the cursor
|
||||
*/
|
||||
var Cursor = function(bson, ns, cmd, options, topology, topologyOptions) {
|
||||
options = options || {};
|
||||
|
||||
// Cursor pool
|
||||
this.pool = null;
|
||||
// Cursor server
|
||||
this.server = null;
|
||||
|
||||
// Do we have a not connected handler
|
||||
this.disconnectHandler = options.disconnectHandler;
|
||||
|
||||
// Set local values
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.cmd = cmd;
|
||||
this.options = options;
|
||||
this.topology = topology;
|
||||
|
||||
// All internal state
|
||||
this.cursorState = {
|
||||
cursorId: null,
|
||||
cmd: cmd,
|
||||
documents: options.documents || [],
|
||||
cursorIndex: 0,
|
||||
dead: false,
|
||||
killed: false,
|
||||
init: false,
|
||||
notified: false,
|
||||
limit: options.limit || cmd.limit || 0,
|
||||
skip: options.skip || cmd.skip || 0,
|
||||
batchSize: options.batchSize || cmd.batchSize || 1000,
|
||||
currentLimit: 0,
|
||||
// Result field name if not a cursor (contains the array of results)
|
||||
transforms: options.transforms,
|
||||
raw: options.raw || (cmd && cmd.raw)
|
||||
};
|
||||
|
||||
if (typeof options.session === 'object') {
|
||||
this.cursorState.session = options.session;
|
||||
}
|
||||
|
||||
// Add promoteLong to cursor state
|
||||
if (typeof topologyOptions.promoteLongs === 'boolean') {
|
||||
this.cursorState.promoteLongs = topologyOptions.promoteLongs;
|
||||
} else if (typeof options.promoteLongs === 'boolean') {
|
||||
this.cursorState.promoteLongs = options.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promoteValues to cursor state
|
||||
if (typeof topologyOptions.promoteValues === 'boolean') {
|
||||
this.cursorState.promoteValues = topologyOptions.promoteValues;
|
||||
} else if (typeof options.promoteValues === 'boolean') {
|
||||
this.cursorState.promoteValues = options.promoteValues;
|
||||
}
|
||||
|
||||
// Add promoteBuffers to cursor state
|
||||
if (typeof topologyOptions.promoteBuffers === 'boolean') {
|
||||
this.cursorState.promoteBuffers = topologyOptions.promoteBuffers;
|
||||
} else if (typeof options.promoteBuffers === 'boolean') {
|
||||
this.cursorState.promoteBuffers = options.promoteBuffers;
|
||||
}
|
||||
|
||||
if (topologyOptions.reconnect) {
|
||||
this.cursorState.reconnect = topologyOptions.reconnect;
|
||||
}
|
||||
|
||||
// Logger
|
||||
this.logger = Logger('Cursor', topologyOptions);
|
||||
|
||||
//
|
||||
// Did we pass in a cursor id
|
||||
if (typeof cmd === 'number') {
|
||||
this.cursorState.cursorId = Long.fromNumber(cmd);
|
||||
this.cursorState.lastCursorId = this.cursorState.cursorId;
|
||||
} else if (cmd instanceof Long) {
|
||||
this.cursorState.cursorId = cmd;
|
||||
this.cursorState.lastCursorId = cmd;
|
||||
}
|
||||
};
|
||||
|
||||
Cursor.prototype.setCursorBatchSize = function(value) {
|
||||
this.cursorState.batchSize = value;
|
||||
};
|
||||
|
||||
Cursor.prototype.cursorBatchSize = function() {
|
||||
return this.cursorState.batchSize;
|
||||
};
|
||||
|
||||
Cursor.prototype.setCursorLimit = function(value) {
|
||||
this.cursorState.limit = value;
|
||||
};
|
||||
|
||||
Cursor.prototype.cursorLimit = function() {
|
||||
return this.cursorState.limit;
|
||||
};
|
||||
|
||||
Cursor.prototype.setCursorSkip = function(value) {
|
||||
this.cursorState.skip = value;
|
||||
};
|
||||
|
||||
Cursor.prototype.cursorSkip = function() {
|
||||
return this.cursorState.skip;
|
||||
};
|
||||
|
||||
Cursor.prototype._endSession = function(options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
const session = this.cursorState.session;
|
||||
|
||||
if (session && (options.force || session.owner === this)) {
|
||||
this.cursorState.session = undefined;
|
||||
session.endSession(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
//
|
||||
// Handle callback (including any exceptions thrown)
|
||||
var handleCallback = function(callback, err, result) {
|
||||
try {
|
||||
callback(err, result);
|
||||
} catch (err) {
|
||||
process.nextTick(function() {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Internal methods
|
||||
Cursor.prototype._getmore = function(callback) {
|
||||
if (this.logger.isDebug())
|
||||
this.logger.debug(f('schedule getMore call for query [%s]', JSON.stringify(this.query)));
|
||||
|
||||
// Set the current batchSize
|
||||
var batchSize = this.cursorState.batchSize;
|
||||
if (
|
||||
this.cursorState.limit > 0 &&
|
||||
this.cursorState.currentLimit + batchSize > this.cursorState.limit
|
||||
) {
|
||||
batchSize = this.cursorState.limit - this.cursorState.currentLimit;
|
||||
}
|
||||
|
||||
this.server.wireProtocolHandler.getMore(
|
||||
this.server,
|
||||
this.ns,
|
||||
this.cursorState,
|
||||
batchSize,
|
||||
this.options,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone the cursor
|
||||
* @method
|
||||
* @return {Cursor}
|
||||
*/
|
||||
Cursor.prototype.clone = function() {
|
||||
return this.topology.cursor(this.ns, this.cmd, this.options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the cursor is dead
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor is dead or not
|
||||
*/
|
||||
Cursor.prototype.isDead = function() {
|
||||
return this.cursorState.dead === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the cursor was killed by the application
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor was killed by the application
|
||||
*/
|
||||
Cursor.prototype.isKilled = function() {
|
||||
return this.cursorState.killed === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the cursor notified it's caller about it's death
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor notified the callback
|
||||
*/
|
||||
Cursor.prototype.isNotified = function() {
|
||||
return this.cursorState.notified === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns current buffered documents length
|
||||
* @method
|
||||
* @return {number} The number of items in the buffered documents
|
||||
*/
|
||||
Cursor.prototype.bufferedCount = function() {
|
||||
return this.cursorState.documents.length - this.cursorState.cursorIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns current buffered documents
|
||||
* @method
|
||||
* @return {Array} An array of buffered documents
|
||||
*/
|
||||
Cursor.prototype.readBufferedDocuments = function(number) {
|
||||
var unreadDocumentsLength = this.cursorState.documents.length - this.cursorState.cursorIndex;
|
||||
var length = number < unreadDocumentsLength ? number : unreadDocumentsLength;
|
||||
var elements = this.cursorState.documents.slice(
|
||||
this.cursorState.cursorIndex,
|
||||
this.cursorState.cursorIndex + length
|
||||
);
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (this.cursorState.transforms && typeof this.cursorState.transforms.doc === 'function') {
|
||||
// Transform all the elements
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i] = this.cursorState.transforms.doc(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we do not return any more documents than the limit imposed
|
||||
// Just return the number of elements up to the limit
|
||||
if (
|
||||
this.cursorState.limit > 0 &&
|
||||
this.cursorState.currentLimit + elements.length > this.cursorState.limit
|
||||
) {
|
||||
elements = elements.slice(0, this.cursorState.limit - this.cursorState.currentLimit);
|
||||
this.kill();
|
||||
}
|
||||
|
||||
// Adjust current limit
|
||||
this.cursorState.currentLimit = this.cursorState.currentLimit + elements.length;
|
||||
this.cursorState.cursorIndex = this.cursorState.cursorIndex + elements.length;
|
||||
|
||||
// Return elements
|
||||
return elements;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kill the cursor
|
||||
* @method
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
Cursor.prototype.kill = function(callback) {
|
||||
// Set cursor to dead
|
||||
this.cursorState.dead = true;
|
||||
this.cursorState.killed = true;
|
||||
// Remove documents
|
||||
this.cursorState.documents = [];
|
||||
|
||||
// If no cursor id just return
|
||||
if (
|
||||
this.cursorState.cursorId == null ||
|
||||
this.cursorState.cursorId.isZero() ||
|
||||
this.cursorState.init === false
|
||||
) {
|
||||
if (callback) callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.wireProtocolHandler.killCursor(this.server, this.ns, this.cursorState, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the cursor
|
||||
* @method
|
||||
* @return {null}
|
||||
*/
|
||||
Cursor.prototype.rewind = function() {
|
||||
if (this.cursorState.init) {
|
||||
if (!this.cursorState.dead) {
|
||||
this.kill();
|
||||
}
|
||||
|
||||
this.cursorState.currentLimit = 0;
|
||||
this.cursorState.init = false;
|
||||
this.cursorState.dead = false;
|
||||
this.cursorState.killed = false;
|
||||
this.cursorState.notified = false;
|
||||
this.cursorState.documents = [];
|
||||
this.cursorState.cursorId = null;
|
||||
this.cursorState.cursorIndex = 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate if the pool is dead and return error
|
||||
*/
|
||||
var isConnectionDead = function(self, callback) {
|
||||
if (self.pool && self.pool.isDestroyed()) {
|
||||
self.cursorState.killed = true;
|
||||
const err = new MongoNetworkError(
|
||||
f('connection to host %s:%s was destroyed', self.pool.host, self.pool.port)
|
||||
);
|
||||
_setCursorNotifiedImpl(self, () => callback(err));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead but was not explicitly killed by user
|
||||
*/
|
||||
var isCursorDeadButNotkilled = function(self, callback) {
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (self.cursorState.dead && !self.cursorState.killed) {
|
||||
self.cursorState.killed = true;
|
||||
setCursorNotified(self, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead and was killed by user
|
||||
*/
|
||||
var isCursorDeadAndKilled = function(self, callback) {
|
||||
if (self.cursorState.dead && self.cursorState.killed) {
|
||||
handleCallback(callback, new MongoError('cursor is dead'));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate if the cursor was killed by the user
|
||||
*/
|
||||
var isCursorKilled = function(self, callback) {
|
||||
if (self.cursorState.killed) {
|
||||
setCursorNotified(self, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark cursor as being dead and notified
|
||||
*/
|
||||
var setCursorDeadAndNotified = function(self, callback) {
|
||||
self.cursorState.dead = true;
|
||||
setCursorNotified(self, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark cursor as being notified
|
||||
*/
|
||||
var setCursorNotified = function(self, callback) {
|
||||
_setCursorNotifiedImpl(self, () => handleCallback(callback, null, null));
|
||||
};
|
||||
|
||||
var _setCursorNotifiedImpl = function(self, callback) {
|
||||
self.cursorState.notified = true;
|
||||
self.cursorState.documents = [];
|
||||
self.cursorState.cursorIndex = 0;
|
||||
if (self._endSession) {
|
||||
return self._endSession(undefined, () => callback());
|
||||
}
|
||||
return callback();
|
||||
};
|
||||
|
||||
var nextFunction = function(self, callback) {
|
||||
// We have notified about it
|
||||
if (self.cursorState.notified) {
|
||||
return callback(new Error('cursor is exhausted'));
|
||||
}
|
||||
|
||||
// Cursor is killed return null
|
||||
if (isCursorKilled(self, callback)) return;
|
||||
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (isCursorDeadButNotkilled(self, callback)) return;
|
||||
|
||||
// We have a dead and killed cursor, attempting to call next should error
|
||||
if (isCursorDeadAndKilled(self, callback)) return;
|
||||
|
||||
// We have just started the cursor
|
||||
if (!self.cursorState.init) {
|
||||
return initializeCursor(self, callback);
|
||||
}
|
||||
|
||||
if (self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
self.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(self, callback);
|
||||
} else if (
|
||||
self.cursorState.cursorIndex === self.cursorState.documents.length &&
|
||||
!Long.ZERO.equals(self.cursorState.cursorId)
|
||||
) {
|
||||
// Ensure an empty cursor state
|
||||
self.cursorState.documents = [];
|
||||
self.cursorState.cursorIndex = 0;
|
||||
|
||||
// Check if topology is destroyed
|
||||
if (self.topology.isDestroyed())
|
||||
return callback(
|
||||
new MongoNetworkError('connection destroyed, not possible to instantiate cursor')
|
||||
);
|
||||
|
||||
// Check if connection is dead and return if not possible to
|
||||
// execute a getmore on this connection
|
||||
if (isConnectionDead(self, callback)) return;
|
||||
|
||||
// Execute the next get more
|
||||
self._getmore(function(err, doc, connection) {
|
||||
if (err) {
|
||||
if (err instanceof MongoError) {
|
||||
err[mongoErrorContextSymbol].isGetMore = true;
|
||||
}
|
||||
|
||||
return handleCallback(callback, err);
|
||||
}
|
||||
|
||||
if (self.cursorState.cursorId && self.cursorState.cursorId.isZero() && self._endSession) {
|
||||
self._endSession();
|
||||
}
|
||||
|
||||
// Save the returned connection to ensure all getMore's fire over the same connection
|
||||
self.connection = connection;
|
||||
|
||||
// Tailable cursor getMore result, notify owner about it
|
||||
// No attempt is made here to retry, this is left to the user of the
|
||||
// core module to handle to keep core simple
|
||||
if (
|
||||
self.cursorState.documents.length === 0 &&
|
||||
self.cmd.tailable &&
|
||||
Long.ZERO.equals(self.cursorState.cursorId)
|
||||
) {
|
||||
// No more documents in the tailed cursor
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: self.cmd.tailable,
|
||||
awaitData: self.cmd.awaitData
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
self.cursorState.documents.length === 0 &&
|
||||
self.cmd.tailable &&
|
||||
!Long.ZERO.equals(self.cursorState.cursorId)
|
||||
) {
|
||||
return nextFunction(self, callback);
|
||||
}
|
||||
|
||||
if (self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) {
|
||||
return setCursorDeadAndNotified(self, callback);
|
||||
}
|
||||
|
||||
nextFunction(self, callback);
|
||||
});
|
||||
} else if (
|
||||
self.cursorState.documents.length === self.cursorState.cursorIndex &&
|
||||
self.cmd.tailable &&
|
||||
Long.ZERO.equals(self.cursorState.cursorId)
|
||||
) {
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: self.cmd.tailable,
|
||||
awaitData: self.cmd.awaitData
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
self.cursorState.documents.length === self.cursorState.cursorIndex &&
|
||||
Long.ZERO.equals(self.cursorState.cursorId)
|
||||
) {
|
||||
setCursorDeadAndNotified(self, callback);
|
||||
} else {
|
||||
if (self.cursorState.limit > 0 && self.cursorState.currentLimit >= self.cursorState.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
self.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(self, callback);
|
||||
}
|
||||
|
||||
// Increment the current cursor limit
|
||||
self.cursorState.currentLimit += 1;
|
||||
|
||||
// Get the document
|
||||
var doc = self.cursorState.documents[self.cursorState.cursorIndex++];
|
||||
|
||||
// Doc overflow
|
||||
if (!doc || doc.$err) {
|
||||
// Ensure we kill the cursor on the server
|
||||
self.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(self, function() {
|
||||
handleCallback(callback, new MongoError(doc ? doc.$err : undefined));
|
||||
});
|
||||
}
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (self.cursorState.transforms && typeof self.cursorState.transforms.doc === 'function') {
|
||||
doc = self.cursorState.transforms.doc(doc);
|
||||
}
|
||||
|
||||
// Return the document
|
||||
handleCallback(callback, null, doc);
|
||||
}
|
||||
};
|
||||
|
||||
function initializeCursor(cursor, callback) {
|
||||
// Topology is not connected, save the call in the provided store to be
|
||||
// Executed at some point when the handler deems it's reconnected
|
||||
if (!cursor.topology.isConnected(cursor.options)) {
|
||||
// Only need this for single server, because repl sets and mongos
|
||||
// will always continue trying to reconnect
|
||||
if (cursor.topology._type === 'server' && !cursor.topology.s.options.reconnect) {
|
||||
// Reconnect is disabled, so we'll never reconnect
|
||||
return callback(new MongoError('no connection available'));
|
||||
}
|
||||
|
||||
if (cursor.disconnectHandler != null) {
|
||||
if (cursor.topology.isDestroyed()) {
|
||||
// Topology was destroyed, so don't try to wait for it to reconnect
|
||||
return callback(new MongoError('Topology was destroyed'));
|
||||
}
|
||||
|
||||
return cursor.disconnectHandler.addObjectAndMethod(
|
||||
'cursor',
|
||||
cursor,
|
||||
'next',
|
||||
[callback],
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return cursor.topology.selectServer(cursor.options, (err, server) => {
|
||||
if (err) {
|
||||
const disconnectHandler = cursor.disconnectHandler;
|
||||
if (disconnectHandler != null) {
|
||||
return disconnectHandler.addObjectAndMethod('cursor', cursor, 'next', [callback], callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
cursor.server = server;
|
||||
cursor.cursorState.init = true;
|
||||
if (collationNotSupported(cursor.server, cursor.cmd)) {
|
||||
return callback(new MongoError(`server ${cursor.server.name} does not support collation`));
|
||||
}
|
||||
|
||||
function done() {
|
||||
if (
|
||||
cursor.cursorState.cursorId &&
|
||||
cursor.cursorState.cursorId.isZero() &&
|
||||
cursor._endSession
|
||||
) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.cursorState.documents.length === 0 &&
|
||||
cursor.cursorState.cursorId &&
|
||||
cursor.cursorState.cursorId.isZero() &&
|
||||
!cursor.cmd.tailable &&
|
||||
!cursor.cmd.awaitData
|
||||
) {
|
||||
return setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
}
|
||||
|
||||
// NOTE: this is a special internal method for cloning a cursor, consider removing
|
||||
if (cursor.cursorState.cursorId != null) {
|
||||
return done();
|
||||
}
|
||||
|
||||
const queryCallback = (err, r) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const result = r.message;
|
||||
if (result.queryFailure) {
|
||||
return callback(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 callback(new MongoError(result.documents[0]), null);
|
||||
}
|
||||
|
||||
// We have a cursor document
|
||||
if (result.documents[0].cursor != null && typeof result.documents[0].cursor !== 'string') {
|
||||
var 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();
|
||||
}
|
||||
|
||||
// Return after processing command cursor
|
||||
return done(result);
|
||||
}
|
||||
|
||||
if (Array.isArray(result.documents[0].result)) {
|
||||
cursor.cursorState.documents = result.documents[0].result;
|
||||
cursor.cursorState.cursorId = Long.ZERO;
|
||||
return done(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to regular find path
|
||||
cursor.cursorState.cursorId = result.cursorId;
|
||||
cursor.cursorState.documents = result.documents;
|
||||
cursor.cursorState.lastCursorId = result.cursorId;
|
||||
|
||||
// Transform the results with passed in transformation method if provided
|
||||
if (
|
||||
cursor.cursorState.transforms &&
|
||||
typeof cursor.cursorState.transforms.query === 'function'
|
||||
) {
|
||||
cursor.cursorState.documents = cursor.cursorState.transforms.query(result);
|
||||
}
|
||||
|
||||
// Return callback
|
||||
done(result);
|
||||
};
|
||||
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(
|
||||
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
|
||||
cursor.query
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
|
||||
if (cursor.cmd.find != null) {
|
||||
cursor.server.wireProtocolHandler.query(
|
||||
cursor.server,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.cursorState,
|
||||
cursor.options,
|
||||
queryCallback
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.query = cursor.server.wireProtocolHandler.command(
|
||||
cursor.server,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.options,
|
||||
queryCallback
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next document from the cursor
|
||||
* @method
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
Cursor.prototype.next = function(callback) {
|
||||
nextFunction(this, callback);
|
||||
};
|
||||
|
||||
module.exports = Cursor;
|
146
node_modules/mongodb-core/lib/error.js
generated
vendored
Normal file
146
node_modules/mongodb-core/lib/error.js
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const mongoErrorContextSymbol = Symbol('mongoErrorContextSymbol');
|
||||
|
||||
/**
|
||||
* Creates a new MongoError
|
||||
*
|
||||
* @augments Error
|
||||
* @param {Error|string|object} message The error message
|
||||
* @property {string} message The error message
|
||||
* @property {string} stack The error call stack
|
||||
*/
|
||||
class MongoError extends Error {
|
||||
constructor(message) {
|
||||
if (message instanceof Error) {
|
||||
super(message.message);
|
||||
this.stack = message.stack;
|
||||
} else {
|
||||
if (typeof message === 'string') {
|
||||
super(message);
|
||||
} else {
|
||||
super(message.message || message.errmsg || message.$err || 'n/a');
|
||||
for (var name in message) {
|
||||
this[name] = message[name];
|
||||
}
|
||||
}
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
this.name = 'MongoError';
|
||||
this[mongoErrorContextSymbol] = this[mongoErrorContextSymbol] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MongoError object
|
||||
*
|
||||
* @param {Error|string|object} options The options used to create the error.
|
||||
* @return {MongoError} A MongoError instance
|
||||
* @deprecated Use `new MongoError()` instead.
|
||||
*/
|
||||
static create(options) {
|
||||
return new MongoError(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MongoNetworkError
|
||||
*
|
||||
* @param {Error|string|object} message The error message
|
||||
* @property {string} message The error message
|
||||
* @property {string} stack The error call stack
|
||||
*/
|
||||
class MongoNetworkError extends MongoError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'MongoNetworkError';
|
||||
|
||||
// This is added as part of the transactions specification
|
||||
this.errorLabels = ['TransientTransactionError'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error used when attempting to parse a value (like a connection string)
|
||||
*
|
||||
* @param {Error|string|object} message The error message
|
||||
* @property {string} message The error message
|
||||
*/
|
||||
class MongoParseError extends MongoError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'MongoParseError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error signifying a timeout event
|
||||
*
|
||||
* @param {Error|string|object} message The error message
|
||||
* @property {string} message The error message
|
||||
*/
|
||||
class MongoTimeoutError extends MongoError {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'MongoTimeoutError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error thrown when the server reports a writeConcernError
|
||||
*
|
||||
* @param {Error|string|object} message The error message
|
||||
* @param {object} result The result document (provided if ok: 1)
|
||||
* @property {string} message The error message
|
||||
* @property {object} [result] The result document (provided if ok: 1)
|
||||
*/
|
||||
class MongoWriteConcernError extends MongoError {
|
||||
constructor(message, result) {
|
||||
super(message);
|
||||
this.name = 'MongoWriteConcernError';
|
||||
|
||||
if (result != null) {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see: https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.rst#terms
|
||||
const RETRYABLE_ERROR_CODES = new Set([
|
||||
6, // HostUnreachable
|
||||
7, // HostNotFound
|
||||
89, // NetworkTimeout
|
||||
91, // ShutdownInProgress
|
||||
189, // PrimarySteppedDown
|
||||
9001, // SocketException
|
||||
10107, // NotMaster
|
||||
11600, // InterruptedAtShutdown
|
||||
11602, // InterruptedDueToReplStateChange
|
||||
13435, // NotMasterNoSlaveOk
|
||||
13436 // NotMasterOrSecondary
|
||||
]);
|
||||
|
||||
/**
|
||||
* Determines whether an error is something the driver should attempt to retry
|
||||
*
|
||||
* @param {MongoError|Error} error
|
||||
*/
|
||||
function isRetryableError(error) {
|
||||
return (
|
||||
RETRYABLE_ERROR_CODES.has(error.code) ||
|
||||
error instanceof MongoNetworkError ||
|
||||
error.message.match(/not master/) ||
|
||||
error.message.match(/node is recovering/)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MongoError,
|
||||
MongoNetworkError,
|
||||
MongoParseError,
|
||||
MongoTimeoutError,
|
||||
MongoWriteConcernError,
|
||||
mongoErrorContextSymbol,
|
||||
isRetryableError
|
||||
};
|
749
node_modules/mongodb-core/lib/sdam/cursor.js
generated
vendored
Normal file
749
node_modules/mongodb-core/lib/sdam/cursor.js
generated
vendored
Normal file
@@ -0,0 +1,749 @@
|
||||
'use strict';
|
||||
|
||||
const Logger = require('../connection/logger');
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const mongoErrorContextSymbol = require('../error').mongoErrorContextSymbol;
|
||||
const Long = BSON.Long;
|
||||
const deprecate = require('util').deprecate;
|
||||
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
|
||||
/**
|
||||
* Handle callback (including any exceptions thrown)
|
||||
*/
|
||||
function handleCallback(callback, err, result) {
|
||||
try {
|
||||
callback(err, result);
|
||||
} catch (err) {
|
||||
process.nextTick(function() {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a cursor results callback
|
||||
*
|
||||
* @callback resultCallback
|
||||
* @param {error} error An error object. Set to null if no error present
|
||||
* @param {object} document
|
||||
*/
|
||||
|
||||
/**
|
||||
* An internal class that embodies a cursor on MongoDB, allowing for iteration over the
|
||||
* results returned from a query.
|
||||
*
|
||||
* @property {number} cursorBatchSize The current cursorBatchSize for the cursor
|
||||
* @property {number} cursorLimit The current cursorLimit for the cursor
|
||||
* @property {number} cursorSkip The current cursorSkip for the cursor
|
||||
*/
|
||||
class Cursor {
|
||||
/**
|
||||
* Create a cursor
|
||||
*
|
||||
* @param {object} bson An instance of the BSON parser
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {{object}|Long} cmd The selector (can be a command or a cursorId)
|
||||
* @param {object} [options=null] Optional settings.
|
||||
* @param {object} [options.batchSize=1000] Batchsize for the operation
|
||||
* @param {array} [options.documents=[]] Initial documents list for cursor
|
||||
* @param {object} [options.transforms=null] Transform methods for the cursor results
|
||||
* @param {function} [options.transforms.query] Transform the value returned from the initial query
|
||||
* @param {function} [options.transforms.doc] Transform each document returned from Cursor.prototype.next
|
||||
* @param {object} topology The server topology instance.
|
||||
* @param {object} topologyOptions The server topology options.
|
||||
*/
|
||||
constructor(bson, ns, cmd, options, topology, topologyOptions) {
|
||||
options = options || {};
|
||||
|
||||
// Cursor pool
|
||||
this.pool = null;
|
||||
// Cursor server
|
||||
this.server = null;
|
||||
|
||||
// Do we have a not connected handler
|
||||
this.disconnectHandler = options.disconnectHandler;
|
||||
|
||||
// Set local values
|
||||
this.bson = bson;
|
||||
this.ns = ns;
|
||||
this.cmd = cmd;
|
||||
this.options = options;
|
||||
this.topology = topology;
|
||||
|
||||
// All internal state
|
||||
this.s = {
|
||||
cursorId: null,
|
||||
cmd: cmd,
|
||||
documents: options.documents || [],
|
||||
cursorIndex: 0,
|
||||
dead: false,
|
||||
killed: false,
|
||||
init: false,
|
||||
notified: false,
|
||||
limit: options.limit || cmd.limit || 0,
|
||||
skip: options.skip || cmd.skip || 0,
|
||||
batchSize: options.batchSize || cmd.batchSize || 1000,
|
||||
currentLimit: 0,
|
||||
// Result field name if not a cursor (contains the array of results)
|
||||
transforms: options.transforms
|
||||
};
|
||||
|
||||
if (typeof options.session === 'object') {
|
||||
this.s.session = options.session;
|
||||
}
|
||||
|
||||
// Add promoteLong to cursor state
|
||||
if (typeof topologyOptions.promoteLongs === 'boolean') {
|
||||
this.s.promoteLongs = topologyOptions.promoteLongs;
|
||||
} else if (typeof options.promoteLongs === 'boolean') {
|
||||
this.s.promoteLongs = options.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promoteValues to cursor state
|
||||
if (typeof topologyOptions.promoteValues === 'boolean') {
|
||||
this.s.promoteValues = topologyOptions.promoteValues;
|
||||
} else if (typeof options.promoteValues === 'boolean') {
|
||||
this.s.promoteValues = options.promoteValues;
|
||||
}
|
||||
|
||||
// Add promoteBuffers to cursor state
|
||||
if (typeof topologyOptions.promoteBuffers === 'boolean') {
|
||||
this.s.promoteBuffers = topologyOptions.promoteBuffers;
|
||||
} else if (typeof options.promoteBuffers === 'boolean') {
|
||||
this.s.promoteBuffers = options.promoteBuffers;
|
||||
}
|
||||
|
||||
if (topologyOptions.reconnect) {
|
||||
this.s.reconnect = topologyOptions.reconnect;
|
||||
}
|
||||
|
||||
// Logger
|
||||
this.logger = Logger('Cursor', topologyOptions);
|
||||
|
||||
//
|
||||
// Did we pass in a cursor id
|
||||
if (typeof cmd === 'number') {
|
||||
this.s.cursorId = Long.fromNumber(cmd);
|
||||
this.s.lastCursorId = this.s.cursorId;
|
||||
} else if (cmd instanceof Long) {
|
||||
this.s.cursorId = cmd;
|
||||
this.s.lastCursorId = cmd;
|
||||
}
|
||||
}
|
||||
|
||||
setCursorBatchSize(value) {
|
||||
this.s.batchSize = value;
|
||||
}
|
||||
|
||||
cursorBatchSize() {
|
||||
return this.s.batchSize;
|
||||
}
|
||||
|
||||
setCursorLimit(value) {
|
||||
this.s.limit = value;
|
||||
}
|
||||
|
||||
cursorLimit() {
|
||||
return this.s.limit;
|
||||
}
|
||||
|
||||
setCursorSkip(value) {
|
||||
this.s.skip = value;
|
||||
}
|
||||
|
||||
cursorSkip() {
|
||||
return this.s.skip;
|
||||
}
|
||||
|
||||
_endSession(options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
const session = this.s.session;
|
||||
if (session && (options.force || session.owner === this)) {
|
||||
this.s.session = undefined;
|
||||
session.endSession(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the cursor
|
||||
* @method
|
||||
* @return {Cursor}
|
||||
*/
|
||||
clone() {
|
||||
return this.topology.cursor(this.ns, this.cmd, this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor is dead
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor is dead or not
|
||||
*/
|
||||
isDead() {
|
||||
return this.s.dead === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor was killed by the application
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor was killed by the application
|
||||
*/
|
||||
isKilled() {
|
||||
return this.s.killed === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor notified it's caller about it's death
|
||||
* @method
|
||||
* @return {boolean} A boolean signifying if the cursor notified the callback
|
||||
*/
|
||||
isNotified() {
|
||||
return this.s.notified === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current buffered documents length
|
||||
* @method
|
||||
* @return {number} The number of items in the buffered documents
|
||||
*/
|
||||
bufferedCount() {
|
||||
return this.s.documents.length - this.s.cursorIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the cursor
|
||||
*
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
kill(callback) {
|
||||
// Set cursor to dead
|
||||
this.s.dead = true;
|
||||
this.s.killed = true;
|
||||
// Remove documents
|
||||
this.s.documents = [];
|
||||
|
||||
// If no cursor id just return
|
||||
if (this.s.cursorId == null || this.s.cursorId.isZero() || this.s.init === false) {
|
||||
if (callback) callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
const pool = this.s.server.s.pool;
|
||||
|
||||
// Execute command
|
||||
this.s.server.s.wireProtocolHandler.killCursor(this.bson, this.ns, this.s, pool, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cursor
|
||||
*/
|
||||
rewind() {
|
||||
if (this.s.init) {
|
||||
if (!this.s.dead) {
|
||||
this.kill();
|
||||
}
|
||||
|
||||
this.s.currentLimit = 0;
|
||||
this.s.init = false;
|
||||
this.s.dead = false;
|
||||
this.s.killed = false;
|
||||
this.s.notified = false;
|
||||
this.s.documents = [];
|
||||
this.s.cursorId = null;
|
||||
this.s.cursorIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current buffered documents
|
||||
* @method
|
||||
* @return {Array} An array of buffered documents
|
||||
*/
|
||||
readBufferedDocuments(number) {
|
||||
const unreadDocumentsLength = this.s.documents.length - this.s.cursorIndex;
|
||||
const length = number < unreadDocumentsLength ? number : unreadDocumentsLength;
|
||||
let elements = this.s.documents.slice(this.s.cursorIndex, this.s.cursorIndex + length);
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (this.s.transforms && typeof this.s.transforms.doc === 'function') {
|
||||
// Transform all the elements
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
elements[i] = this.s.transforms.doc(elements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we do not return any more documents than the limit imposed
|
||||
// Just return the number of elements up to the limit
|
||||
if (this.s.limit > 0 && this.s.currentLimit + elements.length > this.s.limit) {
|
||||
elements = elements.slice(0, this.s.limit - this.s.currentLimit);
|
||||
this.kill();
|
||||
}
|
||||
|
||||
// Adjust current limit
|
||||
this.s.currentLimit = this.s.currentLimit + elements.length;
|
||||
this.s.cursorIndex = this.s.cursorIndex + elements.length;
|
||||
|
||||
// Return elements
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the next document from the cursor
|
||||
*
|
||||
* @param {resultCallback} callback A callback function
|
||||
*/
|
||||
next(callback) {
|
||||
nextFunction(this, callback);
|
||||
}
|
||||
}
|
||||
|
||||
Cursor.prototype._find = deprecate(
|
||||
callback => _find(this, callback),
|
||||
'_find() is deprecated, please stop using it'
|
||||
);
|
||||
|
||||
Cursor.prototype._getmore = deprecate(
|
||||
callback => _getmore(this, callback),
|
||||
'_getmore() is deprecated, please stop using it'
|
||||
);
|
||||
|
||||
function _getmore(cursor, callback) {
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(`schedule getMore call for query [${JSON.stringify(cursor.query)}]`);
|
||||
}
|
||||
|
||||
// Determine if it's a raw query
|
||||
const raw = cursor.options.raw || cursor.cmd.raw;
|
||||
|
||||
// Set the current batchSize
|
||||
let batchSize = cursor.s.batchSize;
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit + batchSize > cursor.s.limit) {
|
||||
batchSize = cursor.s.limit - cursor.s.currentLimit;
|
||||
}
|
||||
|
||||
// Default pool
|
||||
const pool = cursor.s.server.s.pool;
|
||||
|
||||
// We have a wire protocol handler
|
||||
cursor.s.server.s.wireProtocolHandler.getMore(
|
||||
cursor.bson,
|
||||
cursor.ns,
|
||||
cursor.s,
|
||||
batchSize,
|
||||
raw,
|
||||
pool,
|
||||
cursor.options,
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function _find(cursor, callback) {
|
||||
if (cursor.logger.isDebug()) {
|
||||
cursor.logger.debug(
|
||||
`issue initial query [${JSON.stringify(cursor.cmd)}] with flags [${JSON.stringify(
|
||||
cursor.query
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
|
||||
const queryCallback = (err, r) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Get the raw message
|
||||
const result = r.message;
|
||||
|
||||
// Query failure bit set
|
||||
if (result.queryFailure) {
|
||||
return callback(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)) &&
|
||||
(result.documents[0].cursor !== 'string' ||
|
||||
result.documents[0]['$err'] ||
|
||||
result.documents[0]['errmsg'] ||
|
||||
Array.isArray(result.documents[0].result))
|
||||
) {
|
||||
// We have a an error document return the error
|
||||
if (result.documents[0]['$err'] || result.documents[0]['errmsg']) {
|
||||
return callback(new MongoError(result.documents[0]), 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.s.cursorId = typeof id === 'number' ? Long.fromNumber(id) : id;
|
||||
cursor.s.lastCursorId = cursor.s.cursorId;
|
||||
// If we have a firstBatch set it
|
||||
if (Array.isArray(result.documents[0].cursor.firstBatch)) {
|
||||
cursor.s.documents = result.documents[0].cursor.firstBatch;
|
||||
}
|
||||
|
||||
// Return after processing command cursor
|
||||
return callback(null, result);
|
||||
}
|
||||
|
||||
if (Array.isArray(result.documents[0].result)) {
|
||||
cursor.s.documents = result.documents[0].result;
|
||||
cursor.s.cursorId = Long.ZERO;
|
||||
return callback(null, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to regular find path
|
||||
cursor.s.cursorId = result.cursorId;
|
||||
cursor.s.documents = result.documents;
|
||||
cursor.s.lastCursorId = result.cursorId;
|
||||
|
||||
// Transform the results with passed in transformation method if provided
|
||||
if (cursor.s.transforms && typeof cursor.s.transforms.query === 'function') {
|
||||
cursor.s.documents = cursor.s.transforms.query(result);
|
||||
}
|
||||
|
||||
// Return callback
|
||||
callback(null, result);
|
||||
};
|
||||
|
||||
// Options passed to the pool
|
||||
const queryOptions = {};
|
||||
|
||||
// If we have a raw query decorate the function
|
||||
if (cursor.options.raw || cursor.cmd.raw) {
|
||||
queryOptions.raw = cursor.options.raw || cursor.cmd.raw;
|
||||
}
|
||||
|
||||
// Do we have documentsReturnedIn set on the query
|
||||
if (typeof cursor.query.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = cursor.query.documentsReturnedIn;
|
||||
}
|
||||
|
||||
// Add promote Long value if defined
|
||||
if (typeof cursor.s.promoteLongs === 'boolean') {
|
||||
queryOptions.promoteLongs = cursor.s.promoteLongs;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof cursor.s.promoteValues === 'boolean') {
|
||||
queryOptions.promoteValues = cursor.s.promoteValues;
|
||||
}
|
||||
|
||||
// Add promote values if defined
|
||||
if (typeof cursor.s.promoteBuffers === 'boolean') {
|
||||
queryOptions.promoteBuffers = cursor.s.promoteBuffers;
|
||||
}
|
||||
|
||||
if (typeof cursor.s.session === 'object') {
|
||||
queryOptions.session = cursor.s.session;
|
||||
}
|
||||
|
||||
// Write the initial command out
|
||||
cursor.s.server.s.pool.write(cursor.query, queryOptions, queryCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the pool is dead and return error
|
||||
*/
|
||||
function isConnectionDead(cursor, callback) {
|
||||
if (cursor.pool && cursor.pool.isDestroyed()) {
|
||||
cursor.s.killed = true;
|
||||
const err = new MongoNetworkError(
|
||||
`connection to host ${cursor.pool.host}:${cursor.pool.port} was destroyed`
|
||||
);
|
||||
_setCursorNotifiedImpl(cursor, () => callback(err));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead but was not explicitly killed by user
|
||||
*/
|
||||
function isCursorDeadButNotkilled(cursor, callback) {
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (cursor.s.dead && !cursor.s.killed) {
|
||||
cursor.s.killed = true;
|
||||
setCursorNotified(cursor, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor is dead and was killed by user
|
||||
*/
|
||||
function isCursorDeadAndKilled(cursor, callback) {
|
||||
if (cursor.s.dead && cursor.s.killed) {
|
||||
handleCallback(callback, new MongoError('cursor is dead'));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the cursor was killed by the user
|
||||
*/
|
||||
function isCursorKilled(cursor, callback) {
|
||||
if (cursor.s.killed) {
|
||||
setCursorNotified(cursor, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark cursor as being dead and notified
|
||||
*/
|
||||
function setCursorDeadAndNotified(cursor, callback) {
|
||||
cursor.s.dead = true;
|
||||
setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark cursor as being notified
|
||||
*/
|
||||
function setCursorNotified(cursor, callback) {
|
||||
_setCursorNotifiedImpl(cursor, () => handleCallback(callback, null, null));
|
||||
}
|
||||
|
||||
function _setCursorNotifiedImpl(cursor, callback) {
|
||||
cursor.s.notified = true;
|
||||
cursor.s.documents = [];
|
||||
cursor.s.cursorIndex = 0;
|
||||
if (cursor._endSession) {
|
||||
return cursor._endSession(undefined, () => callback());
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
|
||||
function initializeCursorAndRetryNext(cursor, callback) {
|
||||
cursor.topology.selectServer(
|
||||
readPreferenceServerSelector(cursor.options.readPreference || ReadPreference.primary),
|
||||
(err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.s.server = server;
|
||||
cursor.s.init = true;
|
||||
|
||||
// check if server supports collation
|
||||
// NOTE: this should be a part of the selection predicate!
|
||||
if (cursor.cmd && cursor.cmd.collation && cursor.server.description.maxWireVersion < 5) {
|
||||
callback(new MongoError(`server ${cursor.server.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cursor.query = cursor.s.server.s.wireProtocolHandler.command(
|
||||
cursor.bson,
|
||||
cursor.ns,
|
||||
cursor.cmd,
|
||||
cursor.s,
|
||||
cursor.topology,
|
||||
cursor.options
|
||||
);
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function nextFunction(cursor, callback) {
|
||||
// We have notified about it
|
||||
if (cursor.s.notified) {
|
||||
return callback(new Error('cursor is exhausted'));
|
||||
}
|
||||
|
||||
// Cursor is killed return null
|
||||
if (isCursorKilled(cursor, callback)) return;
|
||||
|
||||
// Cursor is dead but not marked killed, return null
|
||||
if (isCursorDeadButNotkilled(cursor, callback)) return;
|
||||
|
||||
// We have a dead and killed cursor, attempting to call next should error
|
||||
if (isCursorDeadAndKilled(cursor, callback)) return;
|
||||
|
||||
// We have just started the cursor
|
||||
if (!cursor.s.init) {
|
||||
return initializeCursorAndRetryNext(cursor, callback);
|
||||
}
|
||||
|
||||
// If we don't have a cursorId execute the first query
|
||||
if (cursor.s.cursorId == null) {
|
||||
// Check if pool is dead and return if not possible to
|
||||
// execute the query against the db
|
||||
if (isConnectionDead(cursor, callback)) return;
|
||||
|
||||
// query, cmd, options, s, callback
|
||||
return _find(cursor, function(err) {
|
||||
if (err) return handleCallback(callback, err, null);
|
||||
|
||||
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.s.cursorId &&
|
||||
cursor.s.cursorId.isZero() &&
|
||||
!cursor.cmd.tailable &&
|
||||
!cursor.cmd.awaitData
|
||||
) {
|
||||
return setCursorNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.s.documents.length === cursor.s.cursorIndex && Long.ZERO.equals(cursor.s.cursorId)) {
|
||||
setCursorDeadAndNotified(cursor, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
setCursorDeadAndNotified(cursor, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
cursor.s.documents.length === cursor.s.cursorIndex &&
|
||||
cursor.cmd.tailable &&
|
||||
Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: cursor.cmd.tailable,
|
||||
awaitData: cursor.cmd.awaitData
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (cursor.s.cursorIndex === cursor.s.documents.length && !Long.ZERO.equals(cursor.s.cursorId)) {
|
||||
// Ensure an empty cursor state
|
||||
cursor.s.documents = [];
|
||||
cursor.s.cursorIndex = 0;
|
||||
|
||||
// Check if connection is dead and return if not possible to
|
||||
if (isConnectionDead(cursor, callback)) return;
|
||||
|
||||
// Execute the next get more
|
||||
return _getmore(cursor, function(err, doc, connection) {
|
||||
if (err) {
|
||||
if (err instanceof MongoError) {
|
||||
err[mongoErrorContextSymbol].isGetMore = true;
|
||||
}
|
||||
|
||||
return handleCallback(callback, err);
|
||||
}
|
||||
|
||||
if (cursor.s.cursorId && cursor.s.cursorId.isZero() && cursor._endSession) {
|
||||
cursor._endSession();
|
||||
}
|
||||
|
||||
// Save the returned connection to ensure all getMore's fire over the same connection
|
||||
cursor.connection = connection;
|
||||
|
||||
// Tailable cursor getMore result, notify owner about it
|
||||
// No attempt is made here to retry, this is left to the user of the
|
||||
// core module to handle to keep core simple
|
||||
if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.cmd.tailable &&
|
||||
Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
// No more documents in the tailed cursor
|
||||
return handleCallback(
|
||||
callback,
|
||||
new MongoError({
|
||||
message: 'No more documents in tailed cursor',
|
||||
tailable: cursor.cmd.tailable,
|
||||
awaitData: cursor.cmd.awaitData
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
cursor.s.documents.length === 0 &&
|
||||
cursor.cmd.tailable &&
|
||||
!Long.ZERO.equals(cursor.s.cursorId)
|
||||
) {
|
||||
return nextFunction(cursor, callback);
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
return setCursorDeadAndNotified(cursor, callback);
|
||||
}
|
||||
|
||||
nextFunction(cursor, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.s.limit > 0 && cursor.s.currentLimit >= cursor.s.limit) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(cursor, callback);
|
||||
}
|
||||
|
||||
// Increment the current cursor limit
|
||||
cursor.s.currentLimit += 1;
|
||||
|
||||
// Get the document
|
||||
let doc = cursor.s.documents[cursor.s.cursorIndex++];
|
||||
|
||||
// Doc overflow
|
||||
if (!doc || doc.$err) {
|
||||
// Ensure we kill the cursor on the server
|
||||
cursor.kill();
|
||||
// Set cursor in dead and notified state
|
||||
return setCursorDeadAndNotified(cursor, function() {
|
||||
handleCallback(callback, new MongoError(doc ? doc.$err : undefined));
|
||||
});
|
||||
}
|
||||
|
||||
// Transform the doc with passed in transformation method if provided
|
||||
if (cursor.s.transforms && typeof cursor.s.transforms.doc === 'function') {
|
||||
doc = cursor.s.transforms.doc(doc);
|
||||
}
|
||||
|
||||
// Return the document
|
||||
handleCallback(callback, null, doc);
|
||||
}
|
||||
|
||||
module.exports = Cursor;
|
217
node_modules/mongodb-core/lib/sdam/monitoring.js
generated
vendored
Normal file
217
node_modules/mongodb-core/lib/sdam/monitoring.js
generated
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
'use strict';
|
||||
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
|
||||
/**
|
||||
* Published when server description changes, but does NOT include changes to the RTT.
|
||||
*
|
||||
* @property {Object} topologyId A unique identifier for the topology
|
||||
* @property {ServerAddress} address The address (host/port pair) of the server
|
||||
* @property {ServerDescription} previousDescription The previous server description
|
||||
* @property {ServerDescription} newDescription The new server description
|
||||
*/
|
||||
class ServerDescriptionChangedEvent {
|
||||
constructor(topologyId, address, previousDescription, newDescription) {
|
||||
Object.assign(this, { topologyId, address, previousDescription, newDescription });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Published when server is initialized.
|
||||
*
|
||||
* @property {Object} topologyId A unique identifier for the topology
|
||||
* @property {ServerAddress} address The address (host/port pair) of the server
|
||||
*/
|
||||
class ServerOpeningEvent {
|
||||
constructor(topologyId, address) {
|
||||
Object.assign(this, { topologyId, address });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Published when server is closed.
|
||||
*
|
||||
* @property {ServerAddress} address The address (host/port pair) of the server
|
||||
* @property {Object} topologyId A unique identifier for the topology
|
||||
*/
|
||||
class ServerClosedEvent {
|
||||
constructor(topologyId, address) {
|
||||
Object.assign(this, { topologyId, address });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Published when topology description changes.
|
||||
*
|
||||
* @property {Object} topologyId
|
||||
* @property {TopologyDescription} previousDescription The old topology description
|
||||
* @property {TopologyDescription} newDescription The new topology description
|
||||
*/
|
||||
class TopologyDescriptionChangedEvent {
|
||||
constructor(topologyId, previousDescription, newDescription) {
|
||||
Object.assign(this, { topologyId, previousDescription, newDescription });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Published when topology is initialized.
|
||||
*
|
||||
* @param {Object} topologyId A unique identifier for the topology
|
||||
*/
|
||||
class TopologyOpeningEvent {
|
||||
constructor(topologyId) {
|
||||
Object.assign(this, { topologyId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Published when topology is closed.
|
||||
*
|
||||
* @param {Object} topologyId A unique identifier for the topology
|
||||
*/
|
||||
class TopologyClosedEvent {
|
||||
constructor(topologyId) {
|
||||
Object.assign(this, { topologyId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the server monitor’s ismaster command is started - immediately before
|
||||
* the ismaster command is serialized into raw BSON and written to the socket.
|
||||
*
|
||||
* @property {Object} connectionId The connection id for the command
|
||||
*/
|
||||
class ServerHeartbeatStartedEvent {
|
||||
constructor(connectionId) {
|
||||
Object.assign(this, { connectionId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the server monitor’s ismaster succeeds.
|
||||
*
|
||||
* @param {Number} duration The execution time of the event in ms
|
||||
* @param {Object} reply The command reply
|
||||
* @param {Object} connectionId The connection id for the command
|
||||
*/
|
||||
class ServerHeartbeatSucceededEvent {
|
||||
constructor(duration, reply, connectionId) {
|
||||
Object.assign(this, { duration, reply, connectionId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when the server monitor’s ismaster fails, either with an “ok: 0” or a socket exception.
|
||||
*
|
||||
* @param {Number} duration The execution time of the event in ms
|
||||
* @param {MongoError|Object} failure The command failure
|
||||
* @param {Object} connectionId The connection id for the command
|
||||
*/
|
||||
class ServerHeartbeatFailedEvent {
|
||||
constructor(duration, failure, connectionId) {
|
||||
Object.assign(this, { duration, failure, connectionId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a server check as described by the SDAM spec.
|
||||
*
|
||||
* NOTE: This method automatically reschedules itself, so that there is always an active
|
||||
* monitoring process
|
||||
*
|
||||
* @param {Server} server The server to monitor
|
||||
*/
|
||||
function monitorServer(server) {
|
||||
// executes a single check of a server
|
||||
const checkServer = callback => {
|
||||
let start = process.hrtime();
|
||||
|
||||
// emit a signal indicating we have started the heartbeat
|
||||
server.emit('serverHeartbeatStarted', new ServerHeartbeatStartedEvent(server.name));
|
||||
|
||||
server.command(
|
||||
'admin.$cmd',
|
||||
{ ismaster: true },
|
||||
{
|
||||
monitoring: true,
|
||||
socketTimeout: server.s.options.connectionTimeout || 2000
|
||||
},
|
||||
function(err, result) {
|
||||
let duration = calculateDurationInMs(start);
|
||||
|
||||
if (err) {
|
||||
server.emit(
|
||||
'serverHeartbeatFailed',
|
||||
new ServerHeartbeatFailedEvent(duration, err, server.name)
|
||||
);
|
||||
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
const isMaster = result.result;
|
||||
server.emit(
|
||||
'serverHeartbeatSucceded',
|
||||
new ServerHeartbeatSucceededEvent(duration, isMaster, server.name)
|
||||
);
|
||||
|
||||
return callback(null, isMaster);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const successHandler = isMaster => {
|
||||
server.s.monitoring = false;
|
||||
|
||||
// emit an event indicating that our description has changed
|
||||
server.emit('descriptionReceived', new ServerDescription(server.description.address, isMaster));
|
||||
|
||||
// schedule the next monitoring process
|
||||
server.s.monitorId = setTimeout(
|
||||
() => monitorServer(server),
|
||||
server.s.options.heartbeatFrequencyMS
|
||||
);
|
||||
};
|
||||
|
||||
// run the actual monitoring loop
|
||||
server.s.monitoring = true;
|
||||
checkServer((err, isMaster) => {
|
||||
if (!err) {
|
||||
successHandler(isMaster);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
// TODO: we need to reset the pool here
|
||||
|
||||
return checkServer((err, isMaster) => {
|
||||
if (err) {
|
||||
server.s.monitoring = false;
|
||||
|
||||
// revert to `Unknown` by emitting a default description with no isMaster
|
||||
server.emit('descriptionReceived', new ServerDescription(server.description.address));
|
||||
|
||||
// do not reschedule monitoring in this case
|
||||
return;
|
||||
}
|
||||
|
||||
successHandler(isMaster);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ServerDescriptionChangedEvent,
|
||||
ServerOpeningEvent,
|
||||
ServerClosedEvent,
|
||||
TopologyDescriptionChangedEvent,
|
||||
TopologyOpeningEvent,
|
||||
TopologyClosedEvent,
|
||||
ServerHeartbeatStartedEvent,
|
||||
ServerHeartbeatSucceededEvent,
|
||||
ServerHeartbeatFailedEvent,
|
||||
monitorServer
|
||||
};
|
411
node_modules/mongodb-core/lib/sdam/server.js
generated
vendored
Normal file
411
node_modules/mongodb-core/lib/sdam/server.js
generated
vendored
Normal file
@@ -0,0 +1,411 @@
|
||||
'use strict';
|
||||
const EventEmitter = require('events');
|
||||
const MongoError = require('../error').MongoError;
|
||||
const Pool = require('../connection/pool');
|
||||
const relayEvents = require('../utils').relayEvents;
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support');
|
||||
const ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support');
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const createClientInfo = require('../topologies/shared').createClientInfo;
|
||||
const Logger = require('../connection/logger');
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const monitorServer = require('./monitoring').monitorServer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @fires Server#serverHeartbeatStarted
|
||||
* @fires Server#serverHeartbeatSucceeded
|
||||
* @fires Server#serverHeartbeatFailed
|
||||
*/
|
||||
class Server extends EventEmitter {
|
||||
/**
|
||||
* Create a server
|
||||
*
|
||||
* @param {ServerDescription} description
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(description, options) {
|
||||
super();
|
||||
|
||||
this.s = {
|
||||
// the server description
|
||||
description,
|
||||
// a saved copy of the incoming options
|
||||
options,
|
||||
// the server logger
|
||||
logger: Logger('Server', options),
|
||||
// the bson parser
|
||||
bson: options.bson || new BSON(),
|
||||
// client metadata for the initial handshake
|
||||
clientInfo: createClientInfo(options),
|
||||
// state variable to determine if there is an active server check in progress
|
||||
monitoring: false,
|
||||
// the connection pool
|
||||
pool: null
|
||||
};
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.s.description;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.s.description.address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
*
|
||||
* @param {Array} [options.auth] Array of auth options to apply on connect
|
||||
*/
|
||||
connect(options) {
|
||||
options = options || {};
|
||||
|
||||
// do not allow connect to be called on anything that's not disconnected
|
||||
if (this.s.pool && !this.s.pool.isDisconnected() && !this.s.pool.isDestroyed()) {
|
||||
throw new MongoError(`Server instance in invalid state ${this.s.pool.state}`);
|
||||
}
|
||||
|
||||
// create a pool
|
||||
this.s.pool = new Pool(this, Object.assign(this.s.options, options, { bson: this.s.bson }));
|
||||
|
||||
// Set up listeners
|
||||
this.s.pool.on('connect', connectEventHandler(this));
|
||||
this.s.pool.on('close', closeEventHandler(this));
|
||||
|
||||
// this.s.pool.on('error', errorEventHandler(this));
|
||||
// this.s.pool.on('timeout', timeoutEventHandler(this));
|
||||
// this.s.pool.on('parseError', errorEventHandler(this));
|
||||
// this.s.pool.on('reconnect', reconnectEventHandler(this));
|
||||
// this.s.pool.on('reconnectFailed', errorEventHandler(this));
|
||||
|
||||
// relay all command monitoring events
|
||||
relayEvents(this.s.pool, this, ['commandStarted', 'commandSucceeded', 'commandFailed']);
|
||||
|
||||
// If auth settings have been provided, use them
|
||||
if (options.auth) {
|
||||
this.s.pool.connect.apply(this.s.pool, options.auth);
|
||||
return;
|
||||
}
|
||||
|
||||
this.s.pool.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the server connection
|
||||
*
|
||||
* @param {Boolean} [options.emitClose=false] Emit close event on destroy
|
||||
* @param {Boolean} [options.emitDestroy=false] Emit destroy event on destroy
|
||||
* @param {Boolean} [options.force=false] Force destroy the pool
|
||||
*/
|
||||
destroy(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately schedule monitoring of this server. If there already an attempt being made
|
||||
* this will be a no-op.
|
||||
*/
|
||||
monitor() {
|
||||
if (this.s.monitoring) return;
|
||||
if (this.s.monitorId) clearTimeout(this.s.monitorId);
|
||||
monitorServer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
*
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {object} cmd The command hash
|
||||
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.checkKeys=false] Specify if the bson parser should validate keys.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {Boolean} [options.fullResult=false] Return the full envelope instead of just the result document.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
command(ns, cmd, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
(callback = options), (options = {}), (options = options || {});
|
||||
}
|
||||
|
||||
const error = basicReadValidations(this, options);
|
||||
if (error) {
|
||||
return callback(error, null);
|
||||
}
|
||||
|
||||
// Clone the options
|
||||
options = Object.assign({}, options, { wireProtocolCommand: false });
|
||||
|
||||
// Debug log
|
||||
if (this.s.logger.isDebug()) {
|
||||
this.s.logger.debug(
|
||||
`executing command [${JSON.stringify({ ns, cmd, options })}] against ${this.name}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we have collation support
|
||||
if (this.description.maxWireVersion < 5 && cmd.collation) {
|
||||
callback(new MongoError(`server ${this.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the query object
|
||||
const query = this.s.wireProtocolHandler.command(this, ns, cmd, {}, options);
|
||||
// Set slave OK of the query
|
||||
query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false;
|
||||
|
||||
// write options
|
||||
const writeOptions = {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
command: true,
|
||||
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
||||
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false,
|
||||
requestId: query.requestId,
|
||||
socketTimeout: typeof options.socketTimeout === 'number' ? options.socketTimeout : null,
|
||||
session: options.session || null
|
||||
};
|
||||
|
||||
// write the operation to the pool
|
||||
this.s.pool.write(query, writeOptions, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert one or more documents
|
||||
* @method
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {array} ops An array of documents to insert
|
||||
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {object} [options.writeConcern={}] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
insert(ns, ops, options, callback) {
|
||||
executeWriteOperation({ server: this, op: 'insert', ns, ops }, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one or more update operations
|
||||
* @method
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {array} ops An array of updates
|
||||
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {object} [options.writeConcern={}] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
update(ns, ops, options, callback) {
|
||||
executeWriteOperation({ server: this, op: 'update', ns, ops }, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one or more remove operations
|
||||
* @method
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {array} ops An array of removes
|
||||
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {object} [options.writeConcern={}] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
remove(ns, ops, options, callback) {
|
||||
executeWriteOperation({ server: this, op: 'remove', ns, ops }, options, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function basicWriteValidations(server) {
|
||||
if (!server.s.pool) {
|
||||
return new MongoError('server instance is not connected');
|
||||
}
|
||||
|
||||
if (server.s.pool.isDestroyed()) {
|
||||
return new MongoError('server instance pool was destroyed');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function basicReadValidations(server, options) {
|
||||
const error = basicWriteValidations(server, options);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (options.readPreference && !(options.readPreference instanceof ReadPreference)) {
|
||||
return new MongoError('readPreference must be an instance of ReadPreference');
|
||||
}
|
||||
}
|
||||
|
||||
function executeWriteOperation(args, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
// TODO: once we drop Node 4, use destructuring either here or in arguments.
|
||||
const server = args.server;
|
||||
const op = args.op;
|
||||
const ns = args.ns;
|
||||
const ops = Array.isArray(args.ops) ? args.ops : [args.ops];
|
||||
|
||||
const error = basicWriteValidations(server, options);
|
||||
if (error) {
|
||||
callback(error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have collation support
|
||||
if (server.description.maxWireVersion < 5 && options.collation) {
|
||||
callback(new MongoError(`server ${this.name} does not support collation`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute write
|
||||
return server.s.wireProtocolHandler[op](server.s.pool, ns, server.s.bson, ops, options, callback);
|
||||
}
|
||||
|
||||
function saslSupportedMechs(options) {
|
||||
if (!options) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const authArray = options.auth || [];
|
||||
const authMechanism = authArray[0] || options.authMechanism;
|
||||
const authSource = authArray[1] || options.authSource || options.dbName || 'admin';
|
||||
const user = authArray[2] || options.user;
|
||||
|
||||
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { saslSupportedMechs: `${authSource}.${user}` };
|
||||
}
|
||||
|
||||
function extractIsMasterError(err, result) {
|
||||
if (err) return err;
|
||||
if (result && result.result && result.result.ok === 0) {
|
||||
return new MongoError(result.result);
|
||||
}
|
||||
}
|
||||
|
||||
function executeServerHandshake(server, callback) {
|
||||
// construct an `ismaster` query
|
||||
const compressors =
|
||||
server.s.options.compression && server.s.options.compression.compressors
|
||||
? server.s.options.compression.compressors
|
||||
: [];
|
||||
|
||||
const queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true };
|
||||
const query = new Query(
|
||||
server.s.bson,
|
||||
'admin.$cmd',
|
||||
Object.assign(
|
||||
{ ismaster: true, client: server.s.clientInfo, compression: compressors },
|
||||
saslSupportedMechs(server.s.options)
|
||||
),
|
||||
queryOptions
|
||||
);
|
||||
|
||||
// execute the query
|
||||
server.s.pool.write(
|
||||
query,
|
||||
{ socketTimeout: server.s.options.connectionTimeout || 2000 },
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function configureWireProtocolHandler(ismaster) {
|
||||
// 3.2 wire protocol handler
|
||||
if (ismaster.maxWireVersion >= 4) {
|
||||
return new ThreeTwoWireProtocolSupport();
|
||||
}
|
||||
|
||||
// default to 2.6 wire protocol handler
|
||||
return new TwoSixWireProtocolSupport();
|
||||
}
|
||||
|
||||
function connectEventHandler(server) {
|
||||
return function() {
|
||||
// log information of received information if in info mode
|
||||
// if (server.s.logger.isInfo()) {
|
||||
// var object = err instanceof MongoError ? JSON.stringify(err) : {};
|
||||
// server.s.logger.info(`server ${server.name} fired event ${event} out with message ${object}`);
|
||||
// }
|
||||
|
||||
// begin initial server handshake
|
||||
const start = process.hrtime();
|
||||
executeServerHandshake(server, (err, response) => {
|
||||
// Set initial lastIsMasterMS - is this needed?
|
||||
server.s.lastIsMasterMS = calculateDurationInMs(start);
|
||||
|
||||
const serverError = extractIsMasterError(err, response);
|
||||
if (serverError) {
|
||||
server.emit('error', serverError);
|
||||
return;
|
||||
}
|
||||
|
||||
// extract the ismaster from the server response
|
||||
const isMaster = response.result;
|
||||
|
||||
// compression negotation
|
||||
if (isMaster && isMaster.compression) {
|
||||
const localCompressionInfo = server.s.options.compression;
|
||||
const localCompressors = localCompressionInfo.compressors;
|
||||
for (var i = 0; i < localCompressors.length; i++) {
|
||||
if (isMaster.compression.indexOf(localCompressors[i]) > -1) {
|
||||
server.s.pool.options.agreedCompressor = localCompressors[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (localCompressionInfo.zlibCompressionLevel) {
|
||||
server.s.pool.options.zlibCompressionLevel = localCompressionInfo.zlibCompressionLevel;
|
||||
}
|
||||
}
|
||||
|
||||
// configure the wire protocol handler
|
||||
server.s.wireProtocolHandler = configureWireProtocolHandler(isMaster);
|
||||
|
||||
// log the connection event if requested
|
||||
if (server.s.logger.isInfo()) {
|
||||
server.s.logger.info(
|
||||
`server ${server.name} connected with ismaster [${JSON.stringify(isMaster)}]`
|
||||
);
|
||||
}
|
||||
|
||||
// emit an event indicating that our description has changed
|
||||
server.emit(
|
||||
'descriptionReceived',
|
||||
new ServerDescription(server.description.address, isMaster)
|
||||
);
|
||||
|
||||
// emit a connect event
|
||||
server.emit('connect', isMaster);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function closeEventHandler(server) {
|
||||
return function() {
|
||||
server.emit('close');
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
141
node_modules/mongodb-core/lib/sdam/server_description.js
generated
vendored
Normal file
141
node_modules/mongodb-core/lib/sdam/server_description.js
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
// An enumeration of server types we know about
|
||||
const ServerType = {
|
||||
Standalone: 'Standalone',
|
||||
Mongos: 'Mongos',
|
||||
PossiblePrimary: 'PossiblePrimary',
|
||||
RSPrimary: 'RSPrimary',
|
||||
RSSecondary: 'RSSecondary',
|
||||
RSArbiter: 'RSArbiter',
|
||||
RSOther: 'RSOther',
|
||||
RSGhost: 'RSGhost',
|
||||
Unknown: 'Unknown'
|
||||
};
|
||||
|
||||
const WRITABLE_SERVER_TYPES = new Set([
|
||||
ServerType.RSPrimary,
|
||||
ServerType.Standalone,
|
||||
ServerType.Mongos
|
||||
]);
|
||||
|
||||
const ISMASTER_FIELDS = [
|
||||
'minWireVersion',
|
||||
'maxWireVersion',
|
||||
'me',
|
||||
'hosts',
|
||||
'passives',
|
||||
'arbiters',
|
||||
'tags',
|
||||
'setName',
|
||||
'setVersion',
|
||||
'electionId',
|
||||
'primary',
|
||||
'logicalSessionTimeoutMinutes'
|
||||
];
|
||||
|
||||
/**
|
||||
* The client's view of a single server, based on the most recent ismaster outcome.
|
||||
*
|
||||
* Internal type, not meant to be directly instantiated
|
||||
*/
|
||||
class ServerDescription {
|
||||
/**
|
||||
* Create a ServerDescription
|
||||
* @param {String} address The address of the server
|
||||
* @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)
|
||||
*/
|
||||
constructor(address, ismaster, options) {
|
||||
options = options || {};
|
||||
ismaster = Object.assign(
|
||||
{
|
||||
minWireVersion: 0,
|
||||
maxWireVersion: 0,
|
||||
hosts: [],
|
||||
passives: [],
|
||||
arbiters: [],
|
||||
tags: []
|
||||
},
|
||||
ismaster
|
||||
);
|
||||
|
||||
this.address = address;
|
||||
this.error = null;
|
||||
this.roundTripTime = options.roundTripTime || 0;
|
||||
this.lastUpdateTime = Date.now();
|
||||
this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
|
||||
this.opTime = ismaster.lastWrite ? ismaster.lastWrite.opTime : null;
|
||||
this.type = parseServerType(ismaster);
|
||||
|
||||
// direct mappings
|
||||
ISMASTER_FIELDS.forEach(field => {
|
||||
if (typeof ismaster[field] !== 'undefined') this[field] = ismaster[field];
|
||||
});
|
||||
|
||||
// normalize case for hosts
|
||||
this.hosts = this.hosts.map(host => host.toLowerCase());
|
||||
this.passives = this.passives.map(host => host.toLowerCase());
|
||||
this.arbiters = this.arbiters.map(host => host.toLowerCase());
|
||||
}
|
||||
|
||||
get allHosts() {
|
||||
return this.hosts.concat(this.arbiters).concat(this.passives);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Boolean} Is this server available for reads
|
||||
*/
|
||||
get isReadable() {
|
||||
return this.type === ServerType.RSSecondary || this.isWritable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Boolean} Is this server available for writes
|
||||
*/
|
||||
get isWritable() {
|
||||
return WRITABLE_SERVER_TYPES.has(this.type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an `ismaster` message and determines the server type
|
||||
*
|
||||
* @param {Object} ismaster The `ismaster` message to parse
|
||||
* @return {ServerType}
|
||||
*/
|
||||
function parseServerType(ismaster) {
|
||||
if (!ismaster || !ismaster.ok) {
|
||||
return ServerType.Unknown;
|
||||
}
|
||||
|
||||
if (ismaster.isreplicaset) {
|
||||
return ServerType.RSGhost;
|
||||
}
|
||||
|
||||
if (ismaster.msg && ismaster.msg === 'isdbgrid') {
|
||||
return ServerType.Mongos;
|
||||
}
|
||||
|
||||
if (ismaster.setName) {
|
||||
if (ismaster.hidden) {
|
||||
return ServerType.RSOther;
|
||||
} else if (ismaster.ismaster) {
|
||||
return ServerType.RSPrimary;
|
||||
} else if (ismaster.secondary) {
|
||||
return ServerType.RSSecondary;
|
||||
} else if (ismaster.arbiterOnly) {
|
||||
return ServerType.RSArbiter;
|
||||
} else {
|
||||
return ServerType.RSOther;
|
||||
}
|
||||
}
|
||||
|
||||
return ServerType.Standalone;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ServerDescription,
|
||||
ServerType
|
||||
};
|
206
node_modules/mongodb-core/lib/sdam/server_selectors.js
generated
vendored
Normal file
206
node_modules/mongodb-core/lib/sdam/server_selectors.js
generated
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
'use strict';
|
||||
const ServerType = require('./server_description').ServerType;
|
||||
const TopologyType = require('./topology_description').TopologyType;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const MongoError = require('../error').MongoError;
|
||||
|
||||
// max staleness constants
|
||||
const IDLE_WRITE_PERIOD = 10000;
|
||||
const SMALLEST_MAX_STALENESS_SECONDS = 90;
|
||||
|
||||
function writableServerSelector() {
|
||||
return function(topologyDescription, servers) {
|
||||
return latencyWindowReducer(topologyDescription, servers.filter(s => s.isWritable));
|
||||
};
|
||||
}
|
||||
|
||||
// reducers
|
||||
function maxStalenessReducer(readPreference, topologyDescription, servers) {
|
||||
if (readPreference.maxStalenessSeconds == null || readPreference.maxStalenessSeconds < 0) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
const maxStaleness = readPreference.maxStalenessSeconds;
|
||||
const maxStalenessVariance =
|
||||
(topologyDescription.heartbeatFrequencyMS + IDLE_WRITE_PERIOD) / 1000;
|
||||
if (maxStaleness < maxStalenessVariance) {
|
||||
throw MongoError(`maxStalenessSeconds must be at least ${maxStalenessVariance} seconds`);
|
||||
}
|
||||
|
||||
if (maxStaleness < SMALLEST_MAX_STALENESS_SECONDS) {
|
||||
throw new MongoError(
|
||||
`maxStalenessSeconds must be at least ${SMALLEST_MAX_STALENESS_SECONDS} seconds`
|
||||
);
|
||||
}
|
||||
|
||||
if (topologyDescription.type === TopologyType.ReplicaSetWithPrimary) {
|
||||
const primary = servers.filter(primaryFilter)[0];
|
||||
return servers.reduce((result, server) => {
|
||||
const stalenessMS =
|
||||
server.lastUpdateTime -
|
||||
server.lastWriteDate -
|
||||
(primary.lastUpdateTime - primary.lastWriteDate) +
|
||||
topologyDescription.heartbeatFrequencyMS;
|
||||
|
||||
const staleness = stalenessMS / 1000;
|
||||
if (staleness <= readPreference.maxStalenessSeconds) result.push(server);
|
||||
return result;
|
||||
}, []);
|
||||
} else if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
|
||||
const sMax = servers.reduce((max, s) => (s.lastWriteDate > max.lastWriteDate ? s : max));
|
||||
return servers.reduce((result, server) => {
|
||||
const stalenessMS =
|
||||
sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
|
||||
|
||||
const staleness = stalenessMS / 1000;
|
||||
if (staleness <= readPreference.maxStalenessSeconds) result.push(server);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
function tagSetMatch(tagSet, serverTags) {
|
||||
const keys = Object.keys(tagSet);
|
||||
const serverTagKeys = Object.keys(serverTags);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
if (serverTagKeys.indexOf(key) === -1 || serverTags[key] !== tagSet[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function tagSetReducer(readPreference, servers) {
|
||||
if (
|
||||
readPreference.tags == null ||
|
||||
(Array.isArray(readPreference.tags) && readPreference.tags.length === 0)
|
||||
) {
|
||||
return servers;
|
||||
}
|
||||
|
||||
for (let i = 0; i < readPreference.tags.length; ++i) {
|
||||
const tagSet = readPreference.tags[i];
|
||||
const serversMatchingTagset = servers.reduce((matched, server) => {
|
||||
if (tagSetMatch(tagSet, server.tags)) matched.push(server);
|
||||
return matched;
|
||||
}, []);
|
||||
|
||||
if (serversMatchingTagset.length) {
|
||||
return serversMatchingTagset;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function latencyWindowReducer(topologyDescription, servers) {
|
||||
const low = servers.reduce(
|
||||
(min, server) => (min === -1 ? server.roundTripTime : Math.min(server.roundTripTime, min)),
|
||||
-1
|
||||
);
|
||||
|
||||
const high = low + topologyDescription.localThresholdMS;
|
||||
|
||||
return servers.reduce((result, server) => {
|
||||
if (server.roundTripTime <= high && server.roundTripTime >= low) result.push(server);
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// filters
|
||||
function primaryFilter(server) {
|
||||
return server.type === ServerType.RSPrimary;
|
||||
}
|
||||
|
||||
function secondaryFilter(server) {
|
||||
return server.type === ServerType.RSSecondary;
|
||||
}
|
||||
|
||||
function nearestFilter(server) {
|
||||
return server.type === ServerType.RSSecondary || server.type === ServerType.RSPrimary;
|
||||
}
|
||||
|
||||
function knownFilter(server) {
|
||||
return server.type !== ServerType.Unknown;
|
||||
}
|
||||
|
||||
function readPreferenceServerSelector(readPreference) {
|
||||
if (!readPreference.isValid()) {
|
||||
throw new TypeError('Invalid read preference specified');
|
||||
}
|
||||
|
||||
return function(topologyDescription, servers) {
|
||||
const commonWireVersion = topologyDescription.commonWireVersion;
|
||||
if (
|
||||
commonWireVersion &&
|
||||
(readPreference.minWireVersion && readPreference.minWireVersion > commonWireVersion)
|
||||
) {
|
||||
throw new MongoError(
|
||||
`Minimum wire version '${
|
||||
readPreference.minWireVersion
|
||||
}' required, but found '${commonWireVersion}'`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
topologyDescription.type === TopologyType.Single ||
|
||||
topologyDescription.type === TopologyType.Sharded
|
||||
) {
|
||||
return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
|
||||
}
|
||||
|
||||
if (readPreference.mode === ReadPreference.PRIMARY) {
|
||||
return servers.filter(primaryFilter);
|
||||
}
|
||||
|
||||
if (readPreference.mode === ReadPreference.SECONDARY) {
|
||||
return latencyWindowReducer(
|
||||
topologyDescription,
|
||||
tagSetReducer(
|
||||
readPreference,
|
||||
maxStalenessReducer(readPreference, topologyDescription, servers)
|
||||
)
|
||||
).filter(secondaryFilter);
|
||||
} else if (readPreference.mode === ReadPreference.NEAREST) {
|
||||
return latencyWindowReducer(
|
||||
topologyDescription,
|
||||
tagSetReducer(
|
||||
readPreference,
|
||||
maxStalenessReducer(readPreference, topologyDescription, servers)
|
||||
)
|
||||
).filter(nearestFilter);
|
||||
} else if (readPreference.mode === ReadPreference.SECONDARY_PREFERRED) {
|
||||
const result = latencyWindowReducer(
|
||||
topologyDescription,
|
||||
tagSetReducer(
|
||||
readPreference,
|
||||
maxStalenessReducer(readPreference, topologyDescription, servers)
|
||||
)
|
||||
).filter(secondaryFilter);
|
||||
|
||||
return result.length === 0 ? servers.filter(primaryFilter) : result;
|
||||
} else if (readPreference.mode === ReadPreference.PRIMARY_PREFERRED) {
|
||||
const result = servers.filter(primaryFilter);
|
||||
if (result.length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return latencyWindowReducer(
|
||||
topologyDescription,
|
||||
tagSetReducer(
|
||||
readPreference,
|
||||
maxStalenessReducer(readPreference, topologyDescription, servers)
|
||||
)
|
||||
).filter(secondaryFilter);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
writableServerSelector,
|
||||
readPreferenceServerSelector
|
||||
};
|
666
node_modules/mongodb-core/lib/sdam/topology.js
generated
vendored
Normal file
666
node_modules/mongodb-core/lib/sdam/topology.js
generated
vendored
Normal file
@@ -0,0 +1,666 @@
|
||||
'use strict';
|
||||
const EventEmitter = require('events');
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const TopologyDescription = require('./topology_description').TopologyDescription;
|
||||
const TopologyType = require('./topology_description').TopologyType;
|
||||
const monitoring = require('./monitoring');
|
||||
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
||||
const MongoTimeoutError = require('../error').MongoTimeoutError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const Server = require('./server');
|
||||
const relayEvents = require('../utils').relayEvents;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
|
||||
const writableServerSelector = require('./server_selectors').writableServerSelector;
|
||||
const isRetryableWritesSupported = require('../topologies/shared').isRetryableWritesSupported;
|
||||
const Cursor = require('./cursor');
|
||||
const deprecate = require('util').deprecate;
|
||||
const BSON = require('../connection/utils').retrieveBSON();
|
||||
const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
|
||||
|
||||
// Global state
|
||||
let globalTopologyCounter = 0;
|
||||
|
||||
// Constants
|
||||
const TOPOLOGY_DEFAULTS = {
|
||||
localThresholdMS: 15,
|
||||
serverSelectionTimeoutMS: 10000,
|
||||
heartbeatFrequencyMS: 30000,
|
||||
minHeartbeatIntervalMS: 500
|
||||
};
|
||||
|
||||
/**
|
||||
* A container of server instances representing a connection to a MongoDB topology.
|
||||
*
|
||||
* @fires Topology#serverOpening
|
||||
* @fires Topology#serverClosed
|
||||
* @fires Topology#serverDescriptionChanged
|
||||
* @fires Topology#topologyOpening
|
||||
* @fires Topology#topologyClosed
|
||||
* @fires Topology#topologyDescriptionChanged
|
||||
* @fires Topology#serverHeartbeatStarted
|
||||
* @fires Topology#serverHeartbeatSucceeded
|
||||
* @fires Topology#serverHeartbeatFailed
|
||||
*/
|
||||
class Topology extends EventEmitter {
|
||||
/**
|
||||
* Create a topology
|
||||
*
|
||||
* @param {Array|String} [seedlist] a string list, or array of Server instances to connect to
|
||||
* @param {Object} [options] Optional settings
|
||||
* @param {Number} [options.localThresholdMS=15] The size of the latency window for selecting among multiple suitable servers
|
||||
* @param {Number} [options.serverSelectionTimeoutMS=30000] How long to block for server selection before throwing an error
|
||||
* @param {Number} [options.heartbeatFrequencyMS=10000] The frequency with which topology updates are scheduled
|
||||
*/
|
||||
constructor(seedlist, options) {
|
||||
super();
|
||||
if (typeof options === 'undefined') {
|
||||
options = seedlist;
|
||||
seedlist = [];
|
||||
|
||||
// this is for legacy single server constructor support
|
||||
if (options.host) {
|
||||
seedlist.push({ host: options.host, port: options.port });
|
||||
}
|
||||
}
|
||||
|
||||
seedlist = seedlist || [];
|
||||
options = Object.assign({}, TOPOLOGY_DEFAULTS, options);
|
||||
|
||||
const topologyType = topologyTypeFromSeedlist(seedlist, options);
|
||||
const topologyId = globalTopologyCounter++;
|
||||
const serverDescriptions = seedlist.reduce((result, seed) => {
|
||||
const address = seed.port ? `${seed.host}:${seed.port}` : `${seed.host}:27017`;
|
||||
result.set(address, new ServerDescription(address));
|
||||
return result;
|
||||
}, new Map());
|
||||
|
||||
this.s = {
|
||||
// the id of this topology
|
||||
id: topologyId,
|
||||
// passed in options
|
||||
options: Object.assign({}, options),
|
||||
// initial seedlist of servers to connect to
|
||||
seedlist: seedlist,
|
||||
// the topology description
|
||||
description: new TopologyDescription(
|
||||
topologyType,
|
||||
serverDescriptions,
|
||||
options.replicaSet,
|
||||
null,
|
||||
null,
|
||||
options
|
||||
),
|
||||
serverSelectionTimeoutMS: options.serverSelectionTimeoutMS,
|
||||
heartbeatFrequencyMS: options.heartbeatFrequencyMS,
|
||||
minHeartbeatIntervalMS: options.minHeartbeatIntervalMS,
|
||||
// allow users to override the cursor factory
|
||||
Cursor: options.cursorFactory || Cursor,
|
||||
// the bson parser
|
||||
bson:
|
||||
options.bson ||
|
||||
new BSON([
|
||||
BSON.Binary,
|
||||
BSON.Code,
|
||||
BSON.DBRef,
|
||||
BSON.Decimal128,
|
||||
BSON.Double,
|
||||
BSON.Int32,
|
||||
BSON.Long,
|
||||
BSON.Map,
|
||||
BSON.MaxKey,
|
||||
BSON.MinKey,
|
||||
BSON.ObjectId,
|
||||
BSON.BSONRegExp,
|
||||
BSON.Symbol,
|
||||
BSON.Timestamp
|
||||
])
|
||||
};
|
||||
|
||||
// amend options for server instance creation
|
||||
this.s.options.compression = { compressors: createCompressionInfo(options) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A `TopologyDescription` for this topology
|
||||
*/
|
||||
get description() {
|
||||
return this.s.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* All raw connections
|
||||
* @method
|
||||
* @return {Connection[]}
|
||||
*/
|
||||
connections() {
|
||||
return Array.from(this.s.servers.values()).reduce((result, server) => {
|
||||
return result.concat(server.s.pool.allConnections());
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate server connect
|
||||
*
|
||||
* @param {Object} [options] Optional settings
|
||||
* @param {Array} [options.auth=null] Array of auth options to apply on connect
|
||||
*/
|
||||
connect(/* options */) {
|
||||
// emit SDAM monitoring events
|
||||
this.emit('topologyOpening', new monitoring.TopologyOpeningEvent(this.s.id));
|
||||
|
||||
// emit an event for the topology change
|
||||
this.emit(
|
||||
'topologyDescriptionChanged',
|
||||
new monitoring.TopologyDescriptionChangedEvent(
|
||||
this.s.id,
|
||||
new TopologyDescription(TopologyType.Unknown), // initial is always Unknown
|
||||
this.s.description
|
||||
)
|
||||
);
|
||||
|
||||
connectServers(this, Array.from(this.s.description.servers.values()));
|
||||
this.s.connected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this topology
|
||||
*/
|
||||
close(callback) {
|
||||
// destroy all child servers
|
||||
this.s.servers.forEach(server => server.destroy());
|
||||
|
||||
// emit an event for close
|
||||
this.emit('topologyClosed', new monitoring.TopologyClosedEvent(this.s.id));
|
||||
|
||||
this.s.connected = false;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a server according to the selection predicate provided
|
||||
*
|
||||
* @param {function} [selector] An optional selector to select servers by, defaults to a random selection within a latency window
|
||||
* @return {Server} An instance of a `Server` meeting the criteria of the predicate provided
|
||||
*/
|
||||
selectServer(selector, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign(
|
||||
{},
|
||||
{ serverSelectionTimeoutMS: this.s.serverSelectionTimeoutMS },
|
||||
options
|
||||
);
|
||||
|
||||
selectServers(
|
||||
this,
|
||||
selector,
|
||||
options.serverSelectionTimeoutMS,
|
||||
process.hrtime(),
|
||||
(err, servers) => {
|
||||
if (err) return callback(err, null);
|
||||
callback(null, randomSelection(servers));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal TopologyDescription with a ServerDescription
|
||||
*
|
||||
* @param {object} serverDescription The server to update in the internal list of server descriptions
|
||||
*/
|
||||
serverUpdateHandler(serverDescription) {
|
||||
if (!this.s.description.hasServer(serverDescription.address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// these will be used for monitoring events later
|
||||
const previousTopologyDescription = this.s.description;
|
||||
const previousServerDescription = this.s.description.servers.get(serverDescription.address);
|
||||
|
||||
// first update the TopologyDescription
|
||||
this.s.description = this.s.description.update(serverDescription);
|
||||
|
||||
// emit monitoring events for this change
|
||||
this.emit(
|
||||
'serverDescriptionChanged',
|
||||
new monitoring.ServerDescriptionChangedEvent(
|
||||
this.s.id,
|
||||
serverDescription.address,
|
||||
previousServerDescription,
|
||||
this.s.description.servers.get(serverDescription.address)
|
||||
)
|
||||
);
|
||||
|
||||
// update server list from updated descriptions
|
||||
updateServers(this, serverDescription);
|
||||
|
||||
this.emit(
|
||||
'topologyDescriptionChanged',
|
||||
new monitoring.TopologyDescriptionChangedEvent(
|
||||
this.s.id,
|
||||
previousTopologyDescription,
|
||||
this.s.description
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate using a specified mechanism
|
||||
*
|
||||
* @param {String} mechanism The auth mechanism used for authentication
|
||||
* @param {String} db The db we are authenticating against
|
||||
* @param {Object} options Optional settings for the authenticating mechanism
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
auth(mechanism, db, options, callback) {
|
||||
callback(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout from a database
|
||||
*
|
||||
* @param {String} db The db we are logging out from
|
||||
* @param {authResultCallback} callback A callback function
|
||||
*/
|
||||
logout(db, callback) {
|
||||
callback(null, null);
|
||||
}
|
||||
|
||||
// Basic operation support. Eventually this should be moved into command construction
|
||||
// during the command refactor.
|
||||
|
||||
/**
|
||||
* Insert one or more documents
|
||||
*
|
||||
* @param {String} ns The full qualified namespace for this operation
|
||||
* @param {Array} ops An array of documents to insert
|
||||
* @param {Boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {Object} [options.writeConcern] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields
|
||||
* @param {ClientSession} [options.session] Session to use for the operation
|
||||
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
insert(ns, ops, options, callback) {
|
||||
executeWriteOperation({ topology: this, op: 'insert', ns, ops }, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one or more update operations
|
||||
*
|
||||
* @param {string} ns The fully qualified namespace for this operation
|
||||
* @param {array} ops An array of updates
|
||||
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {object} [options.writeConcern] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields
|
||||
* @param {ClientSession} [options.session] Session to use for the operation
|
||||
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
update(ns, ops, options, callback) {
|
||||
executeWriteOperation({ topology: this, op: 'update', ns, ops }, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one or more remove operations
|
||||
*
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {array} ops An array of removes
|
||||
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
||||
* @param {object} [options.writeConcern={}] Write concern for the operation
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {boolean} [options.retryWrites] Enable retryable writes for this operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
remove(ns, ops, options, callback) {
|
||||
executeWriteOperation({ topology: this, op: 'remove', ns, ops }, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
*
|
||||
* @method
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {object} cmd The command hash
|
||||
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
||||
* @param {Connection} [options.connection] Specify connection object to execute command against
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {opResultCallback} callback A callback function
|
||||
*/
|
||||
command(ns, cmd, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
(callback = options), (options = {}), (options = options || {});
|
||||
}
|
||||
|
||||
const readPreference = options.readPreference ? options.readPreference : ReadPreference.primary;
|
||||
this.selectServer(readPreferenceServerSelector(readPreference), (err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
server.command(ns, cmd, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cursor
|
||||
*
|
||||
* @method
|
||||
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
||||
* @param {object|Long} cmd Can be either a command returning a cursor or a cursorId
|
||||
* @param {object} [options] Options for the cursor
|
||||
* @param {object} [options.batchSize=0] Batchsize for the operation
|
||||
* @param {array} [options.documents=[]] Initial documents list for cursor
|
||||
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
||||
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
||||
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
||||
* @param {ClientSession} [options.session=null] Session to use for the operation
|
||||
* @param {object} [options.topology] The internal topology of the created cursor
|
||||
* @returns {Cursor}
|
||||
*/
|
||||
cursor(ns, cmd, options) {
|
||||
options = options || {};
|
||||
const topology = options.topology || this;
|
||||
const CursorClass = options.cursorFactory || this.s.Cursor;
|
||||
|
||||
return new CursorClass(this.s.bson, ns, cmd, options, topology, this.s.options);
|
||||
}
|
||||
}
|
||||
|
||||
// legacy aliases
|
||||
Topology.prototype.destroy = deprecate(
|
||||
Topology.prototype.close,
|
||||
'destroy() is deprecated, please use close() instead'
|
||||
);
|
||||
|
||||
function topologyTypeFromSeedlist(seedlist, options) {
|
||||
if (seedlist.length === 1 && !options.replicaSet) return TopologyType.Single;
|
||||
if (options.replicaSet) return TopologyType.ReplicaSetNoPrimary;
|
||||
return TopologyType.Unknown;
|
||||
}
|
||||
|
||||
function randomSelection(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects servers using the provided selector
|
||||
*
|
||||
* @private
|
||||
* @param {Topology} topology The topology to select servers from
|
||||
* @param {function} selector The actual predicate used for selecting servers
|
||||
* @param {Number} timeout The max time we are willing wait for selection
|
||||
* @param {Number} start A high precision timestamp for the start of the selection process
|
||||
* @param {function} callback The callback used to convey errors or the resultant servers
|
||||
*/
|
||||
function selectServers(topology, selector, timeout, start, callback) {
|
||||
const serverDescriptions = Array.from(topology.description.servers.values());
|
||||
let descriptions;
|
||||
|
||||
try {
|
||||
descriptions = selector
|
||||
? selector(topology.description, serverDescriptions)
|
||||
: serverDescriptions;
|
||||
} catch (e) {
|
||||
return callback(e, null);
|
||||
}
|
||||
|
||||
if (descriptions.length) {
|
||||
const servers = descriptions.map(description => topology.s.servers.get(description.address));
|
||||
return callback(null, servers);
|
||||
}
|
||||
|
||||
const duration = calculateDurationInMs(start);
|
||||
if (duration >= timeout) {
|
||||
return callback(new MongoTimeoutError(`Server selection timed out after ${timeout} ms`));
|
||||
}
|
||||
|
||||
const retrySelection = () => {
|
||||
// ensure all server monitors attempt monitoring immediately
|
||||
topology.s.servers.forEach(server => server.monitor());
|
||||
|
||||
const iterationTimer = setTimeout(() => {
|
||||
callback(new MongoTimeoutError('Server selection timed out due to monitoring'));
|
||||
}, topology.s.minHeartbeatIntervalMS);
|
||||
|
||||
topology.once('topologyDescriptionChanged', () => {
|
||||
// successful iteration, clear the check timer
|
||||
clearTimeout(iterationTimer);
|
||||
|
||||
// topology description has changed due to monitoring, reattempt server selection
|
||||
selectServers(topology, selector, timeout, start, callback);
|
||||
});
|
||||
};
|
||||
|
||||
// ensure we are connected
|
||||
if (!topology.s.connected) {
|
||||
topology.connect();
|
||||
|
||||
// we want to make sure we're still within the requested timeout window
|
||||
const failToConnectTimer = setTimeout(() => {
|
||||
callback(new MongoTimeoutError('Server selection timed out waiting to connect'));
|
||||
}, timeout - duration);
|
||||
|
||||
topology.once('connect', () => {
|
||||
clearTimeout(failToConnectTimer);
|
||||
retrySelection();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
retrySelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create `Server` instances for all initially known servers, connect them, and assign
|
||||
* them to the passed in `Topology`.
|
||||
*
|
||||
* @param {Topology} topology The topology responsible for the servers
|
||||
* @param {ServerDescription[]} serverDescriptions A list of server descriptions to connect
|
||||
*/
|
||||
function connectServers(topology, serverDescriptions) {
|
||||
topology.s.servers = serverDescriptions.reduce((servers, serverDescription) => {
|
||||
// publish an open event for each ServerDescription created
|
||||
topology.emit(
|
||||
'serverOpening',
|
||||
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
|
||||
);
|
||||
|
||||
const server = new Server(serverDescription, topology.s.options);
|
||||
relayEvents(server, topology, [
|
||||
'serverHeartbeatStarted',
|
||||
'serverHeartbeatSucceeded',
|
||||
'serverHeartbeatFailed'
|
||||
]);
|
||||
|
||||
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
|
||||
server.on('connect', serverConnectEventHandler(server, topology));
|
||||
servers.set(serverDescription.address, server);
|
||||
server.connect();
|
||||
return servers;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
function updateServers(topology, currentServerDescription) {
|
||||
// update the internal server's description
|
||||
if (topology.s.servers.has(currentServerDescription.address)) {
|
||||
const server = topology.s.servers.get(currentServerDescription.address);
|
||||
server.s.description = currentServerDescription;
|
||||
}
|
||||
|
||||
// add new servers for all descriptions we currently don't know about locally
|
||||
for (const serverDescription of topology.description.servers.values()) {
|
||||
if (!topology.s.servers.has(serverDescription.address)) {
|
||||
topology.emit(
|
||||
'serverOpening',
|
||||
new monitoring.ServerOpeningEvent(topology.s.id, serverDescription.address)
|
||||
);
|
||||
|
||||
const server = new Server(serverDescription, topology.s.options);
|
||||
relayEvents(server, topology, [
|
||||
'serverHeartbeatStarted',
|
||||
'serverHeartbeatSucceeded',
|
||||
'serverHeartbeatFailed'
|
||||
]);
|
||||
|
||||
server.on('descriptionReceived', topology.serverUpdateHandler.bind(topology));
|
||||
server.on('connect', serverConnectEventHandler(server, topology));
|
||||
topology.s.servers.set(serverDescription.address, server);
|
||||
server.connect();
|
||||
}
|
||||
}
|
||||
|
||||
// for all servers no longer known, remove their descriptions and destroy their instances
|
||||
for (const entry of topology.s.servers) {
|
||||
const serverAddress = entry[0];
|
||||
if (topology.description.hasServer(serverAddress)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const server = topology.s.servers.get(serverAddress);
|
||||
topology.s.servers.delete(serverAddress);
|
||||
|
||||
server.destroy(() =>
|
||||
topology.emit('serverClosed', new monitoring.ServerClosedEvent(topology.s.id, serverAddress))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function serverConnectEventHandler(server, topology) {
|
||||
return function(/* ismaster */) {
|
||||
topology.emit('connect', topology);
|
||||
};
|
||||
}
|
||||
|
||||
function executeWriteOperation(args, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
// TODO: once we drop Node 4, use destructuring either here or in arguments.
|
||||
const topology = args.topology;
|
||||
const op = args.op;
|
||||
const ns = args.ns;
|
||||
const ops = args.ops;
|
||||
|
||||
const willRetryWrite =
|
||||
!args.retrying &&
|
||||
options.retryWrites &&
|
||||
options.session &&
|
||||
isRetryableWritesSupported(topology) &&
|
||||
!options.session.inTransaction();
|
||||
|
||||
topology.selectServer(writableServerSelector(), (err, server) => {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = (err, result) => {
|
||||
if (!err) return callback(null, result);
|
||||
if (!(err instanceof MongoNetworkError) && !err.message.match(/not master/)) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (willRetryWrite) {
|
||||
const newArgs = Object.assign({}, args, { retrying: true });
|
||||
return executeWriteOperation(newArgs, options, callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
};
|
||||
|
||||
if (callback.operationId) {
|
||||
handler.operationId = callback.operationId;
|
||||
}
|
||||
|
||||
// increment and assign txnNumber
|
||||
if (willRetryWrite) {
|
||||
options.session.incrementTransactionNumber();
|
||||
options.willRetryWrite = willRetryWrite;
|
||||
}
|
||||
|
||||
// execute the write operation
|
||||
server[op](ns, ops, options, handler);
|
||||
|
||||
// we need to increment the statement id if we're in a transaction
|
||||
if (options.session && options.session.inTransaction()) {
|
||||
options.session.incrementStatementId(ops.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A server opening SDAM monitoring event
|
||||
*
|
||||
* @event Topology#serverOpening
|
||||
* @type {ServerOpeningEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A server closed SDAM monitoring event
|
||||
*
|
||||
* @event Topology#serverClosed
|
||||
* @type {ServerClosedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A server description SDAM change monitoring event
|
||||
*
|
||||
* @event Topology#serverDescriptionChanged
|
||||
* @type {ServerDescriptionChangedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology open SDAM event
|
||||
*
|
||||
* @event Topology#topologyOpening
|
||||
* @type {TopologyOpeningEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology closed SDAM event
|
||||
*
|
||||
* @event Topology#topologyClosed
|
||||
* @type {TopologyClosedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology structure SDAM change event
|
||||
*
|
||||
* @event Topology#topologyDescriptionChanged
|
||||
* @type {TopologyDescriptionChangedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology serverHeartbeatStarted SDAM event
|
||||
*
|
||||
* @event Topology#serverHeartbeatStarted
|
||||
* @type {ServerHeartbeatStartedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology serverHeartbeatFailed SDAM event
|
||||
*
|
||||
* @event Topology#serverHeartbeatFailed
|
||||
* @type {ServerHearbeatFailedEvent}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A topology serverHeartbeatSucceeded SDAM change event
|
||||
*
|
||||
* @event Topology#serverHeartbeatSucceeded
|
||||
* @type {ServerHeartbeatSucceededEvent}
|
||||
*/
|
||||
|
||||
module.exports = Topology;
|
364
node_modules/mongodb-core/lib/sdam/topology_description.js
generated
vendored
Normal file
364
node_modules/mongodb-core/lib/sdam/topology_description.js
generated
vendored
Normal file
@@ -0,0 +1,364 @@
|
||||
'use strict';
|
||||
const ServerType = require('./server_description').ServerType;
|
||||
const ServerDescription = require('./server_description').ServerDescription;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
|
||||
// contstants related to compatability checks
|
||||
const MIN_SUPPORTED_SERVER_VERSION = '2.6';
|
||||
const MIN_SUPPORTED_WIRE_VERSION = 2;
|
||||
const MAX_SUPPORTED_WIRE_VERSION = 5;
|
||||
|
||||
// An enumeration of topology types we know about
|
||||
const TopologyType = {
|
||||
Single: 'Single',
|
||||
ReplicaSetNoPrimary: 'ReplicaSetNoPrimary',
|
||||
ReplicaSetWithPrimary: 'ReplicaSetWithPrimary',
|
||||
Sharded: 'Sharded',
|
||||
Unknown: 'Unknown'
|
||||
};
|
||||
|
||||
// Representation of a deployment of servers
|
||||
class TopologyDescription {
|
||||
/**
|
||||
* Create a TopologyDescription
|
||||
*
|
||||
* @param {string} topologyType
|
||||
* @param {Map<string, ServerDescription>} serverDescriptions the a map of address to ServerDescription
|
||||
* @param {string} setName
|
||||
* @param {number} maxSetVersion
|
||||
* @param {ObjectId} maxElectionId
|
||||
*/
|
||||
constructor(topologyType, serverDescriptions, setName, maxSetVersion, maxElectionId, options) {
|
||||
options = options || {};
|
||||
|
||||
// TODO: consider assigning all these values to a temporary value `s` which
|
||||
// we use `Object.freeze` on, ensuring the internal state of this type
|
||||
// is immutable.
|
||||
this.type = topologyType || TopologyType.Unknown;
|
||||
this.setName = setName || null;
|
||||
this.maxSetVersion = maxSetVersion || null;
|
||||
this.maxElectionId = maxElectionId || null;
|
||||
this.servers = serverDescriptions || new Map();
|
||||
this.stale = false;
|
||||
this.compatible = true;
|
||||
this.compatibilityError = null;
|
||||
this.logicalSessionTimeoutMinutes = null;
|
||||
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS || 0;
|
||||
this.localThresholdMS = options.localThresholdMS || 0;
|
||||
this.options = options;
|
||||
|
||||
// determine server compatibility
|
||||
for (const serverDescription of this.servers.values()) {
|
||||
if (serverDescription.minWireVersion > MAX_SUPPORTED_WIRE_VERSION) {
|
||||
this.compatible = false;
|
||||
this.compatibilityError = `Server at ${serverDescription.address} requires wire version ${
|
||||
serverDescription.minWireVersion
|
||||
}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION}.`;
|
||||
}
|
||||
|
||||
if (serverDescription.maxWireVersion < MIN_SUPPORTED_WIRE_VERSION) {
|
||||
this.compatible = false;
|
||||
this.compatibilityError = `Server at ${serverDescription.address} reports wire version ${
|
||||
serverDescription.maxWireVersion
|
||||
}, but this version of the driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION}).`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever a client updates the TopologyDescription from an ismaster response, it MUST set
|
||||
// TopologyDescription.logicalSessionTimeoutMinutes to the smallest logicalSessionTimeoutMinutes
|
||||
// value among ServerDescriptions of all data-bearing server types. If any have a null
|
||||
// logicalSessionTimeoutMinutes, then TopologyDescription.logicalSessionTimeoutMinutes MUST be
|
||||
// set to null.
|
||||
const readableServers = Array.from(this.servers.values()).filter(s => s.isReadable);
|
||||
this.logicalSessionTimeoutMinutes = readableServers.reduce((result, server) => {
|
||||
if (server.logicalSessionTimeoutMinutes == null) return null;
|
||||
if (result == null) return server.logicalSessionTimeoutMinutes;
|
||||
return Math.min(result, server.logicalSessionTimeoutMinutes);
|
||||
}, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The minimum reported wire version of all known servers
|
||||
*/
|
||||
get commonWireVersion() {
|
||||
return Array.from(this.servers.values())
|
||||
.filter(server => server.type !== ServerType.Unknown)
|
||||
.reduce(
|
||||
(min, server) =>
|
||||
min == null ? server.maxWireVersion : Math.min(min, server.maxWireVersion),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this description updated with a given ServerDescription
|
||||
*
|
||||
* @param {ServerDescription} serverDescription
|
||||
*/
|
||||
update(serverDescription) {
|
||||
const address = serverDescription.address;
|
||||
// NOTE: there are a number of prime targets for refactoring here
|
||||
// once we support destructuring assignments
|
||||
|
||||
// potentially mutated values
|
||||
let topologyType = this.type;
|
||||
let setName = this.setName;
|
||||
let maxSetVersion = this.maxSetVersion;
|
||||
let maxElectionId = this.maxElectionId;
|
||||
|
||||
const serverType = serverDescription.type;
|
||||
let serverDescriptions = new Map(this.servers);
|
||||
|
||||
// update the actual server description
|
||||
serverDescriptions.set(address, serverDescription);
|
||||
|
||||
if (topologyType === TopologyType.Single) {
|
||||
// once we are defined as single, that never changes
|
||||
return new TopologyDescription(
|
||||
TopologyType.Single,
|
||||
serverDescriptions,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.Unknown) {
|
||||
if (serverType === ServerType.Standalone) {
|
||||
serverDescriptions.delete(address);
|
||||
} else {
|
||||
topologyType = topologyTypeForServerType(serverType);
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.Sharded) {
|
||||
if ([ServerType.Mongos, ServerType.Unknown].indexOf(serverType) === -1) {
|
||||
serverDescriptions.delete(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.ReplicaSetNoPrimary) {
|
||||
if ([ServerType.Mongos, ServerType.Unknown].indexOf(serverType) >= 0) {
|
||||
serverDescriptions.delete(address);
|
||||
}
|
||||
|
||||
if (serverType === ServerType.RSPrimary) {
|
||||
const result = updateRsFromPrimary(
|
||||
serverDescriptions,
|
||||
setName,
|
||||
serverDescription,
|
||||
maxSetVersion,
|
||||
maxElectionId
|
||||
);
|
||||
|
||||
(topologyType = result[0]),
|
||||
(setName = result[1]),
|
||||
(maxSetVersion = result[2]),
|
||||
(maxElectionId = result[3]);
|
||||
} else if (
|
||||
[ServerType.RSSecondary, ServerType.RSArbiter, ServerType.RSOther].indexOf(serverType) >= 0
|
||||
) {
|
||||
const result = updateRsNoPrimaryFromMember(serverDescriptions, setName, serverDescription);
|
||||
(topologyType = result[0]), (setName = result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (topologyType === TopologyType.ReplicaSetWithPrimary) {
|
||||
if ([ServerType.Standalone, ServerType.Mongos].indexOf(serverType) >= 0) {
|
||||
serverDescriptions.delete(address);
|
||||
topologyType = checkHasPrimary(serverDescriptions);
|
||||
} else if (serverType === ServerType.RSPrimary) {
|
||||
const result = updateRsFromPrimary(
|
||||
serverDescriptions,
|
||||
setName,
|
||||
serverDescription,
|
||||
maxSetVersion,
|
||||
maxElectionId
|
||||
);
|
||||
|
||||
(topologyType = result[0]),
|
||||
(setName = result[1]),
|
||||
(maxSetVersion = result[2]),
|
||||
(maxElectionId = result[3]);
|
||||
} else if (
|
||||
[ServerType.RSSecondary, ServerType.RSArbiter, ServerType.RSOther].indexOf(serverType) >= 0
|
||||
) {
|
||||
topologyType = updateRsWithPrimaryFromMember(
|
||||
serverDescriptions,
|
||||
setName,
|
||||
serverDescription
|
||||
);
|
||||
} else {
|
||||
topologyType = checkHasPrimary(serverDescriptions);
|
||||
}
|
||||
}
|
||||
|
||||
return new TopologyDescription(
|
||||
topologyType,
|
||||
serverDescriptions,
|
||||
setName,
|
||||
maxSetVersion,
|
||||
maxElectionId,
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the topology has a readable server available. See the table in the
|
||||
* following section for behaviour rules.
|
||||
*
|
||||
* @param {ReadPreference} [readPreference] An optional read preference for determining if a readable server is present
|
||||
* @return {Boolean} Whether there is a readable server in this topology
|
||||
*/
|
||||
hasReadableServer(/* readPreference */) {
|
||||
// To be implemented when server selection is implemented
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the topology has a writable server available. See the table in the
|
||||
* following section for behaviour rules.
|
||||
*
|
||||
* @return {Boolean} Whether there is a writable server in this topology
|
||||
*/
|
||||
hasWritableServer() {
|
||||
return this.hasReadableServer(ReadPreference.primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the topology has a definition for the provided address
|
||||
*
|
||||
* @param {String} address
|
||||
* @return {Boolean} Whether the topology knows about this server
|
||||
*/
|
||||
hasServer(address) {
|
||||
return this.servers.has(address);
|
||||
}
|
||||
}
|
||||
|
||||
function topologyTypeForServerType(serverType) {
|
||||
if (serverType === ServerType.Mongos) return TopologyType.Sharded;
|
||||
if (serverType === ServerType.RSPrimary) return TopologyType.ReplicaSetWithPrimary;
|
||||
return TopologyType.ReplicaSetNoPrimary;
|
||||
}
|
||||
|
||||
function updateRsFromPrimary(
|
||||
serverDescriptions,
|
||||
setName,
|
||||
serverDescription,
|
||||
maxSetVersion,
|
||||
maxElectionId
|
||||
) {
|
||||
setName = setName || serverDescription.setName;
|
||||
if (setName !== serverDescription.setName) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
|
||||
const electionIdOID = serverDescription.electionId ? serverDescription.electionId.$oid : null;
|
||||
const maxElectionIdOID = maxElectionId ? maxElectionId.$oid : null;
|
||||
if (serverDescription.setVersion != null && electionIdOID != null) {
|
||||
if (maxSetVersion != null && maxElectionIdOID != null) {
|
||||
if (maxSetVersion > serverDescription.setVersion || maxElectionIdOID > electionIdOID) {
|
||||
// this primary is stale, we must remove it
|
||||
serverDescriptions.set(
|
||||
serverDescription.address,
|
||||
new ServerDescription(serverDescription.address)
|
||||
);
|
||||
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
}
|
||||
|
||||
maxElectionId = serverDescription.electionId;
|
||||
}
|
||||
|
||||
if (
|
||||
serverDescription.setVersion != null &&
|
||||
(maxSetVersion == null || serverDescription.setVersion > maxSetVersion)
|
||||
) {
|
||||
maxSetVersion = serverDescription.setVersion;
|
||||
}
|
||||
|
||||
// We've heard from the primary. Is it the same primary as before?
|
||||
for (const address of serverDescriptions.keys()) {
|
||||
const server = serverDescriptions.get(address);
|
||||
|
||||
if (server.type === ServerType.RSPrimary && server.address !== serverDescription.address) {
|
||||
// Reset old primary's type to Unknown.
|
||||
serverDescriptions.set(address, new ServerDescription(server.address));
|
||||
|
||||
// There can only be one primary
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Discover new hosts from this primary's response.
|
||||
serverDescription.allHosts.forEach(address => {
|
||||
if (!serverDescriptions.has(address)) {
|
||||
serverDescriptions.set(address, new ServerDescription(address));
|
||||
}
|
||||
});
|
||||
|
||||
// Remove hosts not in the response.
|
||||
const currentAddresses = Array.from(serverDescriptions.keys());
|
||||
const responseAddresses = serverDescription.allHosts;
|
||||
currentAddresses.filter(addr => responseAddresses.indexOf(addr) === -1).forEach(address => {
|
||||
serverDescriptions.delete(address);
|
||||
});
|
||||
|
||||
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||||
}
|
||||
|
||||
function updateRsWithPrimaryFromMember(serverDescriptions, setName, serverDescription) {
|
||||
if (setName == null) {
|
||||
throw new TypeError('setName is required');
|
||||
}
|
||||
|
||||
if (
|
||||
setName !== serverDescription.setName ||
|
||||
(serverDescription.me && serverDescription.address !== serverDescription.me)
|
||||
) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
}
|
||||
|
||||
return checkHasPrimary(serverDescriptions);
|
||||
}
|
||||
|
||||
function updateRsNoPrimaryFromMember(serverDescriptions, setName, serverDescription) {
|
||||
let topologyType = TopologyType.ReplicaSetNoPrimary;
|
||||
|
||||
setName = setName || serverDescription.setName;
|
||||
if (setName !== serverDescription.setName) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
return [topologyType, setName];
|
||||
}
|
||||
|
||||
serverDescription.allHosts.forEach(address => {
|
||||
if (!serverDescriptions.has(address)) {
|
||||
serverDescriptions.set(address, new ServerDescription(address));
|
||||
}
|
||||
});
|
||||
|
||||
if (serverDescription.me && serverDescription.address !== serverDescription.me) {
|
||||
serverDescriptions.delete(serverDescription.address);
|
||||
}
|
||||
|
||||
return [topologyType, setName];
|
||||
}
|
||||
|
||||
function checkHasPrimary(serverDescriptions) {
|
||||
for (const addr of serverDescriptions.keys()) {
|
||||
if (serverDescriptions.get(addr).type === ServerType.RSPrimary) {
|
||||
return TopologyType.ReplicaSetWithPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
return TopologyType.ReplicaSetNoPrimary;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TopologyType,
|
||||
TopologyDescription
|
||||
};
|
459
node_modules/mongodb-core/lib/sessions.js
generated
vendored
Normal file
459
node_modules/mongodb-core/lib/sessions.js
generated
vendored
Normal file
@@ -0,0 +1,459 @@
|
||||
'use strict';
|
||||
|
||||
const retrieveBSON = require('./connection/utils').retrieveBSON;
|
||||
const EventEmitter = require('events');
|
||||
const BSON = retrieveBSON();
|
||||
const Binary = BSON.Binary;
|
||||
const uuidV4 = require('./utils').uuidV4;
|
||||
const MongoError = require('./error').MongoError;
|
||||
const isRetryableError = require('././error').isRetryableError;
|
||||
const MongoNetworkError = require('./error').MongoNetworkError;
|
||||
const MongoWriteConcernError = require('./error').MongoWriteConcernError;
|
||||
const Transaction = require('./transactions').Transaction;
|
||||
const TxnState = require('./transactions').TxnState;
|
||||
|
||||
function assertAlive(session, callback) {
|
||||
if (session.serverSession == null) {
|
||||
const error = new MongoError('Cannot use a session that has ended');
|
||||
if (typeof callback === 'function') {
|
||||
callback(error, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass when creating a Client Session
|
||||
* @typedef {Object} SessionOptions
|
||||
* @property {boolean} [causalConsistency=true] Whether causal consistency should be enabled on this session
|
||||
* @property {TransactionOptions} [defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A BSON document reflecting the lsid of a {@link ClientSession}
|
||||
* @typedef {Object} SessionId
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class representing a client session on the server
|
||||
* WARNING: not meant to be instantiated directly.
|
||||
* @class
|
||||
* @hideconstructor
|
||||
*/
|
||||
class ClientSession extends EventEmitter {
|
||||
/**
|
||||
* Create a client session.
|
||||
* WARNING: not meant to be instantiated directly
|
||||
*
|
||||
* @param {Topology} topology The current client's topology (Internal Class)
|
||||
* @param {ServerSessionPool} sessionPool The server session pool (Internal Class)
|
||||
* @param {SessionOptions} [options] Optional settings
|
||||
* @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
|
||||
*/
|
||||
constructor(topology, sessionPool, options, clientOptions) {
|
||||
super();
|
||||
|
||||
if (topology == null) {
|
||||
throw new Error('ClientSession requires a topology');
|
||||
}
|
||||
|
||||
if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
|
||||
throw new Error('ClientSession requires a ServerSessionPool');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
this.topology = topology;
|
||||
this.sessionPool = sessionPool;
|
||||
this.hasEnded = false;
|
||||
this.serverSession = sessionPool.acquire();
|
||||
this.clientOptions = clientOptions;
|
||||
|
||||
this.supports = {
|
||||
causalConsistency:
|
||||
typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
|
||||
};
|
||||
|
||||
options = options || {};
|
||||
if (typeof options.initialClusterTime !== 'undefined') {
|
||||
this.clusterTime = options.initialClusterTime;
|
||||
} else {
|
||||
this.clusterTime = null;
|
||||
}
|
||||
|
||||
this.operationTime = null;
|
||||
this.explicit = !!options.explicit;
|
||||
this.owner = options.owner;
|
||||
this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
|
||||
this.transaction = new Transaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* The server id associated with this session
|
||||
* @type {SessionId}
|
||||
*/
|
||||
get id() {
|
||||
return this.serverSession.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends this session on the server
|
||||
*
|
||||
* @param {Object} [options] Optional settings. Currently reserved for future use
|
||||
* @param {Function} [callback] Optional callback for completion of this operation
|
||||
*/
|
||||
endSession(options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
if (this.hasEnded) {
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.serverSession && this.inTransaction()) {
|
||||
this.abortTransaction(); // pass in callback?
|
||||
}
|
||||
|
||||
// mark the session as ended, and emit a signal
|
||||
this.hasEnded = true;
|
||||
this.emit('ended', this);
|
||||
|
||||
// release the server session back to the pool
|
||||
this.sessionPool.release(this.serverSession);
|
||||
|
||||
// spec indicates that we should ignore all errors for `endSessions`
|
||||
if (typeof callback === 'function') callback(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the operationTime for a ClientSession.
|
||||
*
|
||||
* @param {Timestamp} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
|
||||
*/
|
||||
advanceOperationTime(operationTime) {
|
||||
if (this.operationTime == null) {
|
||||
this.operationTime = operationTime;
|
||||
return;
|
||||
}
|
||||
|
||||
if (operationTime.greaterThan(this.operationTime)) {
|
||||
this.operationTime = operationTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine if this session equals another
|
||||
* @param {ClientSession} session
|
||||
* @return {boolean} true if the sessions are equal
|
||||
*/
|
||||
equals(session) {
|
||||
if (!(session instanceof ClientSession)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.id.id.buffer.equals(session.id.id.buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the transaction number on the internal ServerSession
|
||||
*/
|
||||
incrementTransactionNumber() {
|
||||
this.serverSession.txnNumber++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} whether this session is currently in a transaction or not
|
||||
*/
|
||||
inTransaction() {
|
||||
return this.transaction.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new transaction with the given options.
|
||||
*
|
||||
* @param {TransactionOptions} options Options for the transaction
|
||||
*/
|
||||
startTransaction(options) {
|
||||
assertAlive(this);
|
||||
if (this.inTransaction()) {
|
||||
throw new MongoError('Transaction already in progress');
|
||||
}
|
||||
|
||||
// increment txnNumber
|
||||
this.incrementTransactionNumber();
|
||||
|
||||
// create transaction state
|
||||
this.transaction = new Transaction(
|
||||
Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
|
||||
);
|
||||
|
||||
this.transaction.transition(TxnState.STARTING_TRANSACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the currently active transaction in this session.
|
||||
*
|
||||
* @param {Function} [callback] optional callback for completion of this operation
|
||||
* @return {Promise} A promise is returned if no callback is provided
|
||||
*/
|
||||
commitTransaction(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
endTransaction(this, 'commitTransaction', callback);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
endTransaction(
|
||||
this,
|
||||
'commitTransaction',
|
||||
(err, reply) => (err ? reject(err) : resolve(reply))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts the currently active transaction in this session.
|
||||
*
|
||||
* @param {Function} [callback] optional callback for completion of this operation
|
||||
* @return {Promise} A promise is returned if no callback is provided
|
||||
*/
|
||||
abortTransaction(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
endTransaction(this, 'abortTransaction', callback);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
endTransaction(
|
||||
this,
|
||||
'abortTransaction',
|
||||
(err, reply) => (err ? reject(err) : resolve(reply))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is here to ensure that ClientSession is never serialized to BSON.
|
||||
* @ignore
|
||||
*/
|
||||
toBSON() {
|
||||
throw new Error('ClientSession cannot be serialized to BSON.');
|
||||
}
|
||||
}
|
||||
|
||||
function endTransaction(session, commandName, callback) {
|
||||
if (!assertAlive(session, callback)) {
|
||||
// checking result in case callback was called
|
||||
return;
|
||||
}
|
||||
|
||||
// handle any initial problematic cases
|
||||
let txnState = session.transaction.state;
|
||||
|
||||
if (txnState === TxnState.NO_TRANSACTION) {
|
||||
callback(new MongoError('No transaction started'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandName === 'commitTransaction') {
|
||||
if (
|
||||
txnState === TxnState.STARTING_TRANSACTION ||
|
||||
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
|
||||
) {
|
||||
// the transaction was never started, we can safely exit here
|
||||
session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (txnState === TxnState.TRANSACTION_ABORTED) {
|
||||
callback(new MongoError('Cannot call commitTransaction after calling abortTransaction'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (txnState === TxnState.STARTING_TRANSACTION) {
|
||||
// the transaction was never started, we can safely exit here
|
||||
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (txnState === TxnState.TRANSACTION_ABORTED) {
|
||||
callback(new MongoError('Cannot call abortTransaction twice'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
txnState === TxnState.TRANSACTION_COMMITTED ||
|
||||
txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
|
||||
) {
|
||||
callback(new MongoError('Cannot call abortTransaction after calling commitTransaction'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// construct and send the command
|
||||
const command = { [commandName]: 1 };
|
||||
|
||||
// apply a writeConcern if specified
|
||||
if (session.transaction.options.writeConcern) {
|
||||
Object.assign(command, { writeConcern: session.transaction.options.writeConcern });
|
||||
} else if (session.clientOptions && session.clientOptions.w) {
|
||||
Object.assign(command, { writeConcern: { w: session.clientOptions.w } });
|
||||
}
|
||||
|
||||
function commandHandler(e, r) {
|
||||
if (commandName === 'commitTransaction') {
|
||||
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
|
||||
|
||||
if (
|
||||
e &&
|
||||
(e instanceof MongoNetworkError ||
|
||||
e instanceof MongoWriteConcernError ||
|
||||
isRetryableError(e))
|
||||
) {
|
||||
if (e.errorLabels) {
|
||||
const idx = e.errorLabels.indexOf('TransientTransactionError');
|
||||
if (idx !== -1) {
|
||||
e.errorLabels.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
e.errorLabels = [];
|
||||
}
|
||||
|
||||
e.errorLabels.push('UnknownTransactionCommitResult');
|
||||
}
|
||||
} else {
|
||||
session.transaction.transition(TxnState.TRANSACTION_ABORTED);
|
||||
}
|
||||
|
||||
callback(e, r);
|
||||
}
|
||||
|
||||
// The spec indicates that we should ignore all errors on `abortTransaction`
|
||||
function transactionError(err) {
|
||||
return commandName === 'commitTransaction' ? err : null;
|
||||
}
|
||||
|
||||
// send the command
|
||||
session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
|
||||
if (err && isRetryableError(err)) {
|
||||
return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
|
||||
commandHandler(transactionError(_err), _reply)
|
||||
);
|
||||
}
|
||||
|
||||
commandHandler(transactionError(err), reply);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflects the existence of a session on the server. Can be reused by the session pool.
|
||||
* WARNING: not meant to be instantiated directly. For internal use only.
|
||||
* @ignore
|
||||
*/
|
||||
class ServerSession {
|
||||
constructor() {
|
||||
this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
|
||||
this.lastUse = Date.now();
|
||||
this.txnNumber = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the server session has timed out.
|
||||
* @ignore
|
||||
* @param {Date} sessionTimeoutMinutes The server's "logicalSessionTimeoutMinutes"
|
||||
* @return {boolean} true if the session has timed out.
|
||||
*/
|
||||
hasTimedOut(sessionTimeoutMinutes) {
|
||||
// Take the difference of the lastUse timestamp and now, which will result in a value in
|
||||
// milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
|
||||
const idleTimeMinutes = Math.round(
|
||||
(((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
|
||||
);
|
||||
|
||||
return idleTimeMinutes > sessionTimeoutMinutes - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains a pool of Server Sessions.
|
||||
* For internal use only
|
||||
* @ignore
|
||||
*/
|
||||
class ServerSessionPool {
|
||||
constructor(topology) {
|
||||
if (topology == null) {
|
||||
throw new Error('ServerSessionPool requires a topology');
|
||||
}
|
||||
|
||||
this.topology = topology;
|
||||
this.sessions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends all sessions in the session pool.
|
||||
* @ignore
|
||||
*/
|
||||
endAllPooledSessions() {
|
||||
if (this.sessions.length) {
|
||||
this.topology.endSessions(this.sessions.map(session => session.id));
|
||||
this.sessions = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a Server Session from the pool.
|
||||
* Iterates through each session in the pool, removing any stale sessions
|
||||
* along the way. The first non-stale session found is removed from the
|
||||
* pool and returned. If no non-stale session is found, a new ServerSession
|
||||
* is created.
|
||||
* @ignore
|
||||
* @returns {ServerSession}
|
||||
*/
|
||||
acquire() {
|
||||
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
|
||||
while (this.sessions.length) {
|
||||
const session = this.sessions.shift();
|
||||
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
return new ServerSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a session to the session pool
|
||||
* Adds the session back to the session pool if the session has not timed out yet.
|
||||
* This method also removes any stale sessions from the pool.
|
||||
* @ignore
|
||||
* @param {ServerSession} session The session to release to the pool
|
||||
*/
|
||||
release(session) {
|
||||
const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
|
||||
while (this.sessions.length) {
|
||||
const session = this.sessions[this.sessions.length - 1];
|
||||
if (session.hasTimedOut(sessionTimeoutMinutes)) {
|
||||
this.sessions.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.hasTimedOut(sessionTimeoutMinutes)) {
|
||||
this.sessions.unshift(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ClientSession,
|
||||
ServerSession,
|
||||
ServerSessionPool,
|
||||
TxnState
|
||||
};
|
61
node_modules/mongodb-core/lib/tools/smoke_plugin.js
generated
vendored
Normal file
61
node_modules/mongodb-core/lib/tools/smoke_plugin.js
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
/* Note: because this plugin uses process.on('uncaughtException'), only one
|
||||
* of these can exist at any given time. This plugin and anything else that
|
||||
* uses process.on('uncaughtException') will conflict. */
|
||||
exports.attachToRunner = function(runner, outputFile) {
|
||||
var smokeOutput = { results: [] };
|
||||
var runningTests = {};
|
||||
|
||||
var integraPlugin = {
|
||||
beforeTest: function(test, callback) {
|
||||
test.startTime = Date.now();
|
||||
runningTests[test.name] = test;
|
||||
callback();
|
||||
},
|
||||
afterTest: function(test, callback) {
|
||||
smokeOutput.results.push({
|
||||
status: test.status,
|
||||
start: test.startTime,
|
||||
end: Date.now(),
|
||||
test_file: test.name,
|
||||
exit_code: 0,
|
||||
url: ''
|
||||
});
|
||||
delete runningTests[test.name];
|
||||
callback();
|
||||
},
|
||||
beforeExit: function(obj, callback) {
|
||||
fs.writeFile(outputFile, JSON.stringify(smokeOutput), function() {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// In case of exception, make sure we write file
|
||||
process.on('uncaughtException', function(err) {
|
||||
// Mark all currently running tests as failed
|
||||
for (var testName in runningTests) {
|
||||
smokeOutput.results.push({
|
||||
status: 'fail',
|
||||
start: runningTests[testName].startTime,
|
||||
end: Date.now(),
|
||||
test_file: testName,
|
||||
exit_code: 0,
|
||||
url: ''
|
||||
});
|
||||
}
|
||||
|
||||
// write file
|
||||
fs.writeFileSync(outputFile, JSON.stringify(smokeOutput));
|
||||
|
||||
// Standard NodeJS uncaught exception handler
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
runner.plugin(integraPlugin);
|
||||
return integraPlugin;
|
||||
};
|
1504
node_modules/mongodb-core/lib/topologies/mongos.js
generated
vendored
Normal file
1504
node_modules/mongodb-core/lib/topologies/mongos.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
193
node_modules/mongodb-core/lib/topologies/read_preference.js
generated
vendored
Normal file
193
node_modules/mongodb-core/lib/topologies/read_preference.js
generated
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* The **ReadPreference** class is a class that represents a MongoDB ReadPreference and is
|
||||
* used to construct connections.
|
||||
* @class
|
||||
* @param {string} mode A string describing the read preference mode (primary|primaryPreferred|secondary|secondaryPreferred|nearest)
|
||||
* @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.
|
||||
* @return {ReadPreference}
|
||||
* @example
|
||||
* const ReplSet = require('mongodb-core').ReplSet,
|
||||
* ReadPreference = require('mongodb-core').ReadPreference,
|
||||
* assert = require('assert');
|
||||
*
|
||||
* const server = new ReplSet([{host: 'localhost', port: 30000}], {setName: 'rs'});
|
||||
* // Wait for the connection event
|
||||
* server.on('connect', function(server) {
|
||||
* const cursor = server.cursor(
|
||||
* 'db.test',
|
||||
* { find: 'db.test', query: {} },
|
||||
* { readPreference: new ReadPreference('secondary') }
|
||||
* );
|
||||
*
|
||||
* cursor.next(function(err, doc) {
|
||||
* server.destroy();
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* // Start connecting
|
||||
* server.connect();
|
||||
* @see https://docs.mongodb.com/manual/core/read-preference/
|
||||
*/
|
||||
const ReadPreference = function(mode, tags, options) {
|
||||
// TODO(major): tags MUST be an array of tagsets
|
||||
if (tags && !Array.isArray(tags)) {
|
||||
console.warn(
|
||||
'ReadPreference tags must be an array, this will change in the next major version'
|
||||
);
|
||||
|
||||
if (typeof tags.maxStalenessSeconds !== 'undefined') {
|
||||
// this is likely an options object
|
||||
options = tags;
|
||||
tags = undefined;
|
||||
} else {
|
||||
tags = [tags];
|
||||
}
|
||||
}
|
||||
|
||||
this.mode = mode;
|
||||
this.tags = tags;
|
||||
|
||||
options = options || {};
|
||||
if (options.maxStalenessSeconds != null) {
|
||||
if (options.maxStalenessSeconds <= 0) {
|
||||
throw new TypeError('maxStalenessSeconds must be a positive integer');
|
||||
}
|
||||
|
||||
this.maxStalenessSeconds = options.maxStalenessSeconds;
|
||||
|
||||
// NOTE: The minimum required wire version is 5 for this read preference. If the existing
|
||||
// topology has a lower value then a MongoError will be thrown during server selection.
|
||||
this.minWireVersion = 5;
|
||||
}
|
||||
|
||||
if (this.mode === ReadPreference.PRIMARY || this.mode === true) {
|
||||
if (this.tags && Array.isArray(this.tags) && this.tags.length > 0) {
|
||||
throw new TypeError('Primary read preference cannot be combined with tags');
|
||||
}
|
||||
|
||||
if (this.maxStalenessSeconds) {
|
||||
throw new TypeError('Primary read preference cannot be combined with maxStalenessSeconds');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Support the deprecated `preference` property introduced in the porcelain layer
|
||||
Object.defineProperty(ReadPreference.prototype, 'preference', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return this.mode;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Read preference mode constants
|
||||
*/
|
||||
ReadPreference.PRIMARY = 'primary';
|
||||
ReadPreference.PRIMARY_PREFERRED = 'primaryPreferred';
|
||||
ReadPreference.SECONDARY = 'secondary';
|
||||
ReadPreference.SECONDARY_PREFERRED = 'secondaryPreferred';
|
||||
ReadPreference.NEAREST = 'nearest';
|
||||
|
||||
const VALID_MODES = [
|
||||
ReadPreference.PRIMARY,
|
||||
ReadPreference.PRIMARY_PREFERRED,
|
||||
ReadPreference.SECONDARY,
|
||||
ReadPreference.SECONDARY_PREFERRED,
|
||||
ReadPreference.NEAREST,
|
||||
true,
|
||||
false,
|
||||
null
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate if a mode is legal
|
||||
*
|
||||
* @method
|
||||
* @param {string} mode The string representing the read preference mode.
|
||||
* @return {boolean} True if a mode is valid
|
||||
*/
|
||||
ReadPreference.isValid = function(mode) {
|
||||
return VALID_MODES.indexOf(mode) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate if a mode is legal
|
||||
*
|
||||
* @method
|
||||
* @param {string} mode The string representing the read preference mode.
|
||||
* @return {boolean} True if a mode is valid
|
||||
*/
|
||||
ReadPreference.prototype.isValid = function(mode) {
|
||||
return ReadPreference.isValid(typeof mode === 'string' ? mode : this.mode);
|
||||
};
|
||||
|
||||
const needSlaveOk = ['primaryPreferred', 'secondary', 'secondaryPreferred', 'nearest'];
|
||||
|
||||
/**
|
||||
* Indicates that this readPreference needs the "slaveOk" bit when sent over the wire
|
||||
* @method
|
||||
* @return {boolean}
|
||||
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-query
|
||||
*/
|
||||
ReadPreference.prototype.slaveOk = function() {
|
||||
return needSlaveOk.indexOf(this.mode) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Are the two read preference equal
|
||||
* @method
|
||||
* @param {ReadPreference} readPreference The read preference with which to check equality
|
||||
* @return {boolean} True if the two ReadPreferences are equivalent
|
||||
*/
|
||||
ReadPreference.prototype.equals = function(readPreference) {
|
||||
return readPreference.mode === this.mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return JSON representation
|
||||
* @method
|
||||
* @return {Object} A JSON representation of the ReadPreference
|
||||
*/
|
||||
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;
|
||||
return readPreference;
|
||||
};
|
||||
|
||||
/**
|
||||
* Primary read preference
|
||||
* @member
|
||||
* @type {ReadPreference}
|
||||
*/
|
||||
ReadPreference.primary = new ReadPreference('primary');
|
||||
/**
|
||||
* Primary Preferred read preference
|
||||
* @member
|
||||
* @type {ReadPreference}
|
||||
*/
|
||||
ReadPreference.primaryPreferred = new ReadPreference('primaryPreferred');
|
||||
/**
|
||||
* Secondary read preference
|
||||
* @member
|
||||
* @type {ReadPreference}
|
||||
*/
|
||||
ReadPreference.secondary = new ReadPreference('secondary');
|
||||
/**
|
||||
* Secondary Preferred read preference
|
||||
* @member
|
||||
* @type {ReadPreference}
|
||||
*/
|
||||
ReadPreference.secondaryPreferred = new ReadPreference('secondaryPreferred');
|
||||
/**
|
||||
* Nearest read preference
|
||||
* @member
|
||||
* @type {ReadPreference}
|
||||
*/
|
||||
ReadPreference.nearest = new ReadPreference('nearest');
|
||||
|
||||
module.exports = ReadPreference;
|
1724
node_modules/mongodb-core/lib/topologies/replset.js
generated
vendored
Normal file
1724
node_modules/mongodb-core/lib/topologies/replset.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1099
node_modules/mongodb-core/lib/topologies/replset_state.js
generated
vendored
Normal file
1099
node_modules/mongodb-core/lib/topologies/replset_state.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1117
node_modules/mongodb-core/lib/topologies/server.js
generated
vendored
Normal file
1117
node_modules/mongodb-core/lib/topologies/server.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
434
node_modules/mongodb-core/lib/topologies/shared.js
generated
vendored
Normal file
434
node_modules/mongodb-core/lib/topologies/shared.js
generated
vendored
Normal file
@@ -0,0 +1,434 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
const f = require('util').format;
|
||||
const ReadPreference = require('./read_preference');
|
||||
const Buffer = require('safe-buffer').Buffer;
|
||||
|
||||
/**
|
||||
* Emit event if it exists
|
||||
* @method
|
||||
*/
|
||||
function emitSDAMEvent(self, event, description) {
|
||||
if (self.listeners(event).length > 0) {
|
||||
self.emit(event, description);
|
||||
}
|
||||
}
|
||||
|
||||
// Get package.json variable
|
||||
var driverVersion = require('../../package.json').version;
|
||||
var nodejsversion = f('Node.js %s, %s', process.version, os.endianness());
|
||||
var type = os.type();
|
||||
var name = process.platform;
|
||||
var architecture = process.arch;
|
||||
var release = os.release();
|
||||
|
||||
function createClientInfo(options) {
|
||||
// Build default client information
|
||||
var clientInfo = options.clientInfo
|
||||
? clone(options.clientInfo)
|
||||
: {
|
||||
driver: {
|
||||
name: 'nodejs-core',
|
||||
version: driverVersion
|
||||
},
|
||||
os: {
|
||||
type: type,
|
||||
name: name,
|
||||
architecture: architecture,
|
||||
version: release
|
||||
}
|
||||
};
|
||||
|
||||
// Is platform specified
|
||||
if (clientInfo.platform && clientInfo.platform.indexOf('mongodb-core') === -1) {
|
||||
clientInfo.platform = f('%s, mongodb-core: %s', clientInfo.platform, driverVersion);
|
||||
} else if (!clientInfo.platform) {
|
||||
clientInfo.platform = nodejsversion;
|
||||
}
|
||||
|
||||
// Do we have an application specific string
|
||||
if (options.appname) {
|
||||
// Cut at 128 bytes
|
||||
var buffer = Buffer.from(options.appname);
|
||||
// Return the truncated appname
|
||||
var appname = buffer.length > 128 ? buffer.slice(0, 128).toString('utf8') : options.appname;
|
||||
// Add to the clientInfo
|
||||
clientInfo.application = { name: appname };
|
||||
}
|
||||
|
||||
return clientInfo;
|
||||
}
|
||||
|
||||
function createCompressionInfo(options) {
|
||||
if (!options.compression || !options.compression.compressors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check that all supplied compressors are valid
|
||||
options.compression.compressors.forEach(function(compressor) {
|
||||
if (compressor !== 'snappy' && compressor !== 'zlib') {
|
||||
throw new Error('compressors must be at least one of snappy or zlib');
|
||||
}
|
||||
});
|
||||
|
||||
return options.compression.compressors;
|
||||
}
|
||||
|
||||
function clone(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
var getPreviousDescription = function(self) {
|
||||
if (!self.s.serverDescription) {
|
||||
self.s.serverDescription = {
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: 'Unknown'
|
||||
};
|
||||
}
|
||||
|
||||
return self.s.serverDescription;
|
||||
};
|
||||
|
||||
var emitServerDescriptionChanged = function(self, description) {
|
||||
if (self.listeners('serverDescriptionChanged').length > 0) {
|
||||
// Emit the server description changed events
|
||||
self.emit('serverDescriptionChanged', {
|
||||
topologyId: self.s.topologyId !== -1 ? self.s.topologyId : self.id,
|
||||
address: self.name,
|
||||
previousDescription: getPreviousDescription(self),
|
||||
newDescription: description
|
||||
});
|
||||
|
||||
self.s.serverDescription = description;
|
||||
}
|
||||
};
|
||||
|
||||
var getPreviousTopologyDescription = function(self) {
|
||||
if (!self.s.topologyDescription) {
|
||||
self.s.topologyDescription = {
|
||||
topologyType: 'Unknown',
|
||||
servers: [
|
||||
{
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: 'Unknown'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return self.s.topologyDescription;
|
||||
};
|
||||
|
||||
var emitTopologyDescriptionChanged = function(self, description) {
|
||||
if (self.listeners('topologyDescriptionChanged').length > 0) {
|
||||
// Emit the server description changed events
|
||||
self.emit('topologyDescriptionChanged', {
|
||||
topologyId: self.s.topologyId !== -1 ? self.s.topologyId : self.id,
|
||||
address: self.name,
|
||||
previousDescription: getPreviousTopologyDescription(self),
|
||||
newDescription: description
|
||||
});
|
||||
|
||||
self.s.serverDescription = description;
|
||||
}
|
||||
};
|
||||
|
||||
var changedIsMaster = function(self, currentIsmaster, ismaster) {
|
||||
var currentType = getTopologyType(self, currentIsmaster);
|
||||
var newType = getTopologyType(self, ismaster);
|
||||
if (newType !== currentType) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
var getTopologyType = function(self, ismaster) {
|
||||
if (!ismaster) {
|
||||
ismaster = self.ismaster;
|
||||
}
|
||||
|
||||
if (!ismaster) return 'Unknown';
|
||||
if (ismaster.ismaster && ismaster.msg === 'isdbgrid') return 'Mongos';
|
||||
if (ismaster.ismaster && !ismaster.hosts) return 'Standalone';
|
||||
if (ismaster.ismaster) return 'RSPrimary';
|
||||
if (ismaster.secondary) return 'RSSecondary';
|
||||
if (ismaster.arbiterOnly) return 'RSArbiter';
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
var inquireServerState = function(self) {
|
||||
return function(callback) {
|
||||
if (self.s.state === 'destroyed') return;
|
||||
// Record response time
|
||||
var start = new Date().getTime();
|
||||
|
||||
// emitSDAMEvent
|
||||
emitSDAMEvent(self, 'serverHeartbeatStarted', { connectionId: self.name });
|
||||
|
||||
// Attempt to execute ismaster command
|
||||
self.command('admin.$cmd', { ismaster: true }, { monitoring: true }, function(err, r) {
|
||||
if (!err) {
|
||||
// Legacy event sender
|
||||
self.emit('ismaster', r, self);
|
||||
|
||||
// Calculate latencyMS
|
||||
var latencyMS = new Date().getTime() - start;
|
||||
|
||||
// Server heart beat event
|
||||
emitSDAMEvent(self, 'serverHeartbeatSucceeded', {
|
||||
durationMS: latencyMS,
|
||||
reply: r.result,
|
||||
connectionId: self.name
|
||||
});
|
||||
|
||||
// Did the server change
|
||||
if (changedIsMaster(self, self.s.ismaster, r.result)) {
|
||||
// Emit server description changed if something listening
|
||||
emitServerDescriptionChanged(self, {
|
||||
address: self.name,
|
||||
arbiters: [],
|
||||
hosts: [],
|
||||
passives: [],
|
||||
type: !self.s.inTopology ? 'Standalone' : getTopologyType(self)
|
||||
});
|
||||
}
|
||||
|
||||
// Updat ismaster view
|
||||
self.s.ismaster = r.result;
|
||||
|
||||
// Set server response time
|
||||
self.s.isMasterLatencyMS = latencyMS;
|
||||
} else {
|
||||
emitSDAMEvent(self, 'serverHeartbeatFailed', {
|
||||
durationMS: latencyMS,
|
||||
failure: err,
|
||||
connectionId: self.name
|
||||
});
|
||||
}
|
||||
|
||||
// Peforming an ismaster monitoring callback operation
|
||||
if (typeof callback === 'function') {
|
||||
return callback(err, r);
|
||||
}
|
||||
|
||||
// Perform another sweep
|
||||
self.s.inquireServerStateTimeout = setTimeout(inquireServerState(self), self.s.haInterval);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
//
|
||||
// Clone the options
|
||||
var cloneOptions = function(options) {
|
||||
var opts = {};
|
||||
for (var name in options) {
|
||||
opts[name] = options[name];
|
||||
}
|
||||
return opts;
|
||||
};
|
||||
|
||||
function Interval(fn, time) {
|
||||
var timer = false;
|
||||
|
||||
this.start = function() {
|
||||
if (!this.isRunning()) {
|
||||
timer = setInterval(fn, time);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
this.stop = function() {
|
||||
clearInterval(timer);
|
||||
timer = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
this.isRunning = function() {
|
||||
return timer !== false;
|
||||
};
|
||||
}
|
||||
|
||||
function Timeout(fn, time) {
|
||||
var timer = false;
|
||||
|
||||
this.start = function() {
|
||||
if (!this.isRunning()) {
|
||||
timer = setTimeout(fn, time);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
this.stop = function() {
|
||||
clearTimeout(timer);
|
||||
timer = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
this.isRunning = function() {
|
||||
if (timer && timer._called) return false;
|
||||
return timer !== false;
|
||||
};
|
||||
}
|
||||
|
||||
function diff(previous, current) {
|
||||
// Difference document
|
||||
var diff = {
|
||||
servers: []
|
||||
};
|
||||
|
||||
// Previous entry
|
||||
if (!previous) {
|
||||
previous = { servers: [] };
|
||||
}
|
||||
|
||||
// Check if we have any previous servers missing in the current ones
|
||||
for (var i = 0; i < previous.servers.length; i++) {
|
||||
var found = false;
|
||||
|
||||
for (var j = 0; j < current.servers.length; j++) {
|
||||
if (current.servers[j].address.toLowerCase() === previous.servers[i].address.toLowerCase()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// Add to the diff
|
||||
diff.servers.push({
|
||||
address: previous.servers[i].address,
|
||||
from: previous.servers[i].type,
|
||||
to: 'Unknown'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are any severs that don't exist
|
||||
for (j = 0; j < current.servers.length; j++) {
|
||||
found = false;
|
||||
|
||||
// Go over all the previous servers
|
||||
for (i = 0; i < previous.servers.length; i++) {
|
||||
if (previous.servers[i].address.toLowerCase() === current.servers[j].address.toLowerCase()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the server to the diff
|
||||
if (!found) {
|
||||
diff.servers.push({
|
||||
address: current.servers[j].address,
|
||||
from: 'Unknown',
|
||||
to: current.servers[j].type
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Got through all the servers
|
||||
for (i = 0; i < previous.servers.length; i++) {
|
||||
var prevServer = previous.servers[i];
|
||||
|
||||
// Go through all current servers
|
||||
for (j = 0; j < current.servers.length; j++) {
|
||||
var currServer = current.servers[j];
|
||||
|
||||
// Matching server
|
||||
if (prevServer.address.toLowerCase() === currServer.address.toLowerCase()) {
|
||||
// We had a change in state
|
||||
if (prevServer.type !== currServer.type) {
|
||||
diff.servers.push({
|
||||
address: prevServer.address,
|
||||
from: prevServer.type,
|
||||
to: currServer.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return difference
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared function to determine clusterTime for a given topology
|
||||
*
|
||||
* @param {*} topology
|
||||
* @param {*} clusterTime
|
||||
*/
|
||||
function resolveClusterTime(topology, $clusterTime) {
|
||||
if (topology.clusterTime == null) {
|
||||
topology.clusterTime = $clusterTime;
|
||||
} else {
|
||||
if ($clusterTime.clusterTime.greaterThan(topology.clusterTime.clusterTime)) {
|
||||
topology.clusterTime = $clusterTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: this is a temporary move until the topologies can be more formally refactored
|
||||
// to share code.
|
||||
const SessionMixins = {
|
||||
endSessions: function(sessions, callback) {
|
||||
if (!Array.isArray(sessions)) {
|
||||
sessions = [sessions];
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// When connected to a sharded cluster the endSessions command
|
||||
// can be sent to any mongos. When connected to a replica set the
|
||||
// endSessions command MUST be sent to the primary if the primary
|
||||
// is available, otherwise it MUST be sent to any available secondary.
|
||||
// Is it enough to use: ReadPreference.primaryPreferred ?
|
||||
this.command(
|
||||
'admin.$cmd',
|
||||
{ endSessions: sessions },
|
||||
{ readPreference: ReadPreference.primaryPreferred },
|
||||
() => {
|
||||
// intentionally ignored, per spec
|
||||
if (typeof callback === 'function') callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const RETRYABLE_WIRE_VERSION = 6;
|
||||
|
||||
/**
|
||||
* Determines whether the provided topology supports retryable writes
|
||||
*
|
||||
* @param {Mongos|Replset} topology
|
||||
*/
|
||||
const isRetryableWritesSupported = function(topology) {
|
||||
const maxWireVersion = topology.lastIsMaster().maxWireVersion;
|
||||
if (maxWireVersion < RETRYABLE_WIRE_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!topology.logicalSessionTimeoutMinutes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
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.createClientInfo = createClientInfo;
|
||||
module.exports.createCompressionInfo = createCompressionInfo;
|
||||
module.exports.clone = clone;
|
||||
module.exports.diff = diff;
|
||||
module.exports.Interval = Interval;
|
||||
module.exports.Timeout = Timeout;
|
||||
module.exports.isRetryableWritesSupported = isRetryableWritesSupported;
|
134
node_modules/mongodb-core/lib/transactions.js
generated
vendored
Normal file
134
node_modules/mongodb-core/lib/transactions.js
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
const MongoError = require('./error').MongoError;
|
||||
|
||||
let TxnState;
|
||||
let stateMachine;
|
||||
|
||||
(() => {
|
||||
const NO_TRANSACTION = 'NO_TRANSACTION';
|
||||
const STARTING_TRANSACTION = 'STARTING_TRANSACTION';
|
||||
const TRANSACTION_IN_PROGRESS = 'TRANSACTION_IN_PROGRESS';
|
||||
const TRANSACTION_COMMITTED = 'TRANSACTION_COMMITTED';
|
||||
const TRANSACTION_COMMITTED_EMPTY = 'TRANSACTION_COMMITTED_EMPTY';
|
||||
const TRANSACTION_ABORTED = 'TRANSACTION_ABORTED';
|
||||
|
||||
TxnState = {
|
||||
NO_TRANSACTION,
|
||||
STARTING_TRANSACTION,
|
||||
TRANSACTION_IN_PROGRESS,
|
||||
TRANSACTION_COMMITTED,
|
||||
TRANSACTION_COMMITTED_EMPTY,
|
||||
TRANSACTION_ABORTED
|
||||
};
|
||||
|
||||
stateMachine = {
|
||||
[NO_TRANSACTION]: [NO_TRANSACTION, STARTING_TRANSACTION],
|
||||
[STARTING_TRANSACTION]: [
|
||||
TRANSACTION_IN_PROGRESS,
|
||||
TRANSACTION_COMMITTED,
|
||||
TRANSACTION_COMMITTED_EMPTY,
|
||||
TRANSACTION_ABORTED
|
||||
],
|
||||
[TRANSACTION_IN_PROGRESS]: [
|
||||
TRANSACTION_IN_PROGRESS,
|
||||
TRANSACTION_COMMITTED,
|
||||
TRANSACTION_ABORTED
|
||||
],
|
||||
[TRANSACTION_COMMITTED]: [
|
||||
TRANSACTION_COMMITTED,
|
||||
TRANSACTION_COMMITTED_EMPTY,
|
||||
STARTING_TRANSACTION,
|
||||
NO_TRANSACTION
|
||||
],
|
||||
[TRANSACTION_ABORTED]: [STARTING_TRANSACTION, NO_TRANSACTION],
|
||||
[TRANSACTION_COMMITTED_EMPTY]: [TRANSACTION_COMMITTED_EMPTY, NO_TRANSACTION]
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* The MongoDB ReadConcern, which allows for control of the consistency and isolation properties
|
||||
* of the data read from replica sets and replica set shards.
|
||||
* @typedef {Object} ReadConcern
|
||||
* @property {'local'|'available'|'majority'|'linearizable'|'snapshot'} level The readConcern Level
|
||||
* @see https://docs.mongodb.com/manual/reference/read-concern/
|
||||
*/
|
||||
|
||||
/**
|
||||
* A MongoDB WriteConcern, which describes the level of acknowledgement
|
||||
* requested from MongoDB for write operations.
|
||||
* @typedef {Object} WriteConcern
|
||||
* @property {number|'majority'|string} [w=1] requests acknowledgement that the write operation has
|
||||
* propagated to a specified number of mongod hosts
|
||||
* @property {boolean} [j=false] requests acknowledgement from MongoDB that the write operation has
|
||||
* been written to the journal
|
||||
* @property {number} [wtimeout] a time limit, in milliseconds, for the write concern
|
||||
* @see https://docs.mongodb.com/manual/reference/write-concern/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration options for a transaction.
|
||||
* @typedef {Object} TransactionOptions
|
||||
* @property {ReadConcern} [readConcern] A default read concern for commands in this transaction
|
||||
* @property {WriteConcern} [writeConcern] A default writeConcern for commands in this transaction
|
||||
* @property {ReadPreference} [readPreference] A default read preference for commands in this transaction
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class maintaining state related to a server transaction. Internal Only
|
||||
* @ignore
|
||||
*/
|
||||
class Transaction {
|
||||
/**
|
||||
* Create a transaction
|
||||
*
|
||||
* @ignore
|
||||
* @param {TransactionOptions} [options] Optional settings
|
||||
*/
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.state = TxnState.NO_TRANSACTION;
|
||||
this.options = {};
|
||||
|
||||
if (options.writeConcern || typeof options.w !== 'undefined') {
|
||||
const w = options.writeConcern ? options.writeConcern.w : options.w;
|
||||
if (w <= 0) {
|
||||
throw new MongoError('Transactions do not support unacknowledged write concern');
|
||||
}
|
||||
|
||||
this.options.writeConcern = options.writeConcern ? options.writeConcern : { w: options.w };
|
||||
}
|
||||
|
||||
if (options.readConcern) this.options.readConcern = options.readConcern;
|
||||
if (options.readPreference) this.options.readPreference = options.readPreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @return Whether this session is presently in a transaction
|
||||
*/
|
||||
get isActive() {
|
||||
return (
|
||||
[TxnState.STARTING_TRANSACTION, TxnState.TRANSACTION_IN_PROGRESS].indexOf(this.state) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition the transaction in the state machine
|
||||
* @ignore
|
||||
* @param {TxnState} state The new state to transition to
|
||||
*/
|
||||
transition(nextState) {
|
||||
const nextStates = stateMachine[this.state];
|
||||
if (nextStates && nextStates.indexOf(nextState) !== -1) {
|
||||
this.state = nextState;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new MongoError(
|
||||
`Attempted illegal state transition from [${this.state}] to [${nextState}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TxnState, Transaction };
|
536
node_modules/mongodb-core/lib/uri_parser.js
generated
vendored
Normal file
536
node_modules/mongodb-core/lib/uri_parser.js
generated
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
'use strict';
|
||||
const URL = require('url');
|
||||
const qs = require('querystring');
|
||||
const dns = require('dns');
|
||||
const MongoParseError = require('./error').MongoParseError;
|
||||
const ReadPreference = require('./topologies/read_preference');
|
||||
|
||||
/**
|
||||
* The following regular expression validates a connection string and breaks the
|
||||
* provide string into the following capture groups: [protocol, username, password, hosts]
|
||||
*/
|
||||
const HOSTS_RX = /(mongodb(?:\+srv|)):\/\/(?: (?:[^:]*) (?: : ([^@]*) )? @ )?([^/?]*)(?:\/|)(.*)/;
|
||||
|
||||
/**
|
||||
* Determines whether a provided address matches the provided parent domain in order
|
||||
* to avoid certain attack vectors.
|
||||
*
|
||||
* @param {String} srvAddress The address to check against a domain
|
||||
* @param {String} parentDomain The domain to check the provided address against
|
||||
* @return {Boolean} Whether the provided address matches the parent domain
|
||||
*/
|
||||
function matchesParentDomain(srvAddress, parentDomain) {
|
||||
const regex = /^.*?\./;
|
||||
const srv = `.${srvAddress.replace(regex, '')}`;
|
||||
const parent = `.${parentDomain.replace(regex, '')}`;
|
||||
return srv.endsWith(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a `mongodb+srv` connection string, combine the parts and reparse it as a normal
|
||||
* connection string.
|
||||
*
|
||||
* @param {string} uri The connection string to parse
|
||||
* @param {object} options Optional user provided connection string options
|
||||
* @param {function} callback
|
||||
*/
|
||||
function parseSrvConnectionString(uri, options, callback) {
|
||||
const result = URL.parse(uri, true);
|
||||
|
||||
if (result.hostname.split('.').length < 3) {
|
||||
return callback(new MongoParseError('URI does not have hostname, domain name and tld'));
|
||||
}
|
||||
|
||||
result.domainLength = result.hostname.split('.').length;
|
||||
if (result.pathname && result.pathname.match(',')) {
|
||||
return callback(new MongoParseError('Invalid URI, cannot contain multiple hostnames'));
|
||||
}
|
||||
|
||||
if (result.port) {
|
||||
return callback(new MongoParseError(`Ports not accepted with '${PROTOCOL_MONGODB_SRV}' URIs`));
|
||||
}
|
||||
|
||||
// Resolve the SRV record and use the result as the list of hosts to connect to.
|
||||
const lookupAddress = result.host;
|
||||
dns.resolveSrv(`_mongodb._tcp.${lookupAddress}`, (err, addresses) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return callback(new MongoParseError('No addresses found at host'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < addresses.length; i++) {
|
||||
if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) {
|
||||
return callback(
|
||||
new MongoParseError('Server record does not share hostname with parent URI')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the original URL to a non-SRV URL.
|
||||
result.protocol = 'mongodb';
|
||||
result.host = addresses.map(address => `${address.name}:${address.port}`).join(',');
|
||||
|
||||
// Default to SSL true if it's not specified.
|
||||
if (
|
||||
!('ssl' in options) &&
|
||||
(!result.search || !('ssl' in result.query) || result.query.ssl === null)
|
||||
) {
|
||||
result.query.ssl = true;
|
||||
}
|
||||
|
||||
// Resolve TXT record and add options from there if they exist.
|
||||
dns.resolveTxt(lookupAddress, (err, record) => {
|
||||
if (err) {
|
||||
if (err.code !== 'ENODATA') {
|
||||
return callback(err);
|
||||
}
|
||||
record = null;
|
||||
}
|
||||
|
||||
if (record) {
|
||||
if (record.length > 1) {
|
||||
return callback(new MongoParseError('Multiple text records not allowed'));
|
||||
}
|
||||
|
||||
record = qs.parse(record[0].join(''));
|
||||
if (Object.keys(record).some(key => key !== 'authSource' && key !== 'replicaSet')) {
|
||||
return callback(
|
||||
new MongoParseError('Text record must only set `authSource` or `replicaSet`')
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(result.query, record);
|
||||
}
|
||||
|
||||
// Set completed options back into the URL object.
|
||||
result.search = qs.stringify(result.query);
|
||||
|
||||
const finalString = URL.format(result);
|
||||
parseConnectionString(finalString, options, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a query string item according to the connection string spec
|
||||
*
|
||||
* @param {string} key The key for the parsed value
|
||||
* @param {Array|String} value The value to parse
|
||||
* @return {Array|Object|String} The parsed value
|
||||
*/
|
||||
function parseQueryStringItemValue(key, value) {
|
||||
if (Array.isArray(value)) {
|
||||
// deduplicate and simplify arrays
|
||||
value = value.filter((v, idx) => value.indexOf(v) === idx);
|
||||
if (value.length === 1) value = value[0];
|
||||
} else if (value.indexOf(':') > 0) {
|
||||
value = value.split(',').reduce((result, pair) => {
|
||||
const parts = pair.split(':');
|
||||
result[parts[0]] = parseQueryStringItemValue(key, parts[1]);
|
||||
return result;
|
||||
}, {});
|
||||
} else if (value.indexOf(',') > 0) {
|
||||
value = value.split(',').map(v => {
|
||||
return parseQueryStringItemValue(key, v);
|
||||
});
|
||||
} else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
||||
value = value.toLowerCase() === 'true';
|
||||
} else if (!Number.isNaN(value) && !STRING_OPTIONS.has(key)) {
|
||||
const numericValue = parseFloat(value);
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Options that are known boolean types
|
||||
const BOOLEAN_OPTIONS = new Set([
|
||||
'slaveok',
|
||||
'slave_ok',
|
||||
'sslvalidate',
|
||||
'fsync',
|
||||
'safe',
|
||||
'retrywrites',
|
||||
'j'
|
||||
]);
|
||||
|
||||
// Known string options, only used to bypass Number coercion in `parseQueryStringItemValue`
|
||||
const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
|
||||
|
||||
// Supported text representations of auth mechanisms
|
||||
// NOTE: this list exists in native already, if it is merged here we should deduplicate
|
||||
const AUTH_MECHANISMS = new Set([
|
||||
'GSSAPI',
|
||||
'MONGODB-X509',
|
||||
'MONGODB-CR',
|
||||
'DEFAULT',
|
||||
'SCRAM-SHA-1',
|
||||
'SCRAM-SHA-256',
|
||||
'PLAIN'
|
||||
]);
|
||||
|
||||
// Lookup table used to translate normalized (lower-cased) forms of connection string
|
||||
// options to their expected camelCase version
|
||||
const CASE_TRANSLATION = {
|
||||
replicaset: 'replicaSet',
|
||||
connecttimeoutms: 'connectTimeoutMS',
|
||||
sockettimeoutms: 'socketTimeoutMS',
|
||||
maxpoolsize: 'maxPoolSize',
|
||||
minpoolsize: 'minPoolSize',
|
||||
maxidletimems: 'maxIdleTimeMS',
|
||||
waitqueuemultiple: 'waitQueueMultiple',
|
||||
waitqueuetimeoutms: 'waitQueueTimeoutMS',
|
||||
wtimeoutms: 'wtimeoutMS',
|
||||
readconcern: 'readConcern',
|
||||
readconcernlevel: 'readConcernLevel',
|
||||
readpreference: 'readPreference',
|
||||
maxstalenessseconds: 'maxStalenessSeconds',
|
||||
readpreferencetags: 'readPreferenceTags',
|
||||
authsource: 'authSource',
|
||||
authmechanism: 'authMechanism',
|
||||
authmechanismproperties: 'authMechanismProperties',
|
||||
gssapiservicename: 'gssapiServiceName',
|
||||
localthresholdms: 'localThresholdMS',
|
||||
serverselectiontimeoutms: 'serverSelectionTimeoutMS',
|
||||
serverselectiontryonce: 'serverSelectionTryOnce',
|
||||
heartbeatfrequencyms: 'heartbeatFrequencyMS',
|
||||
appname: 'appName',
|
||||
retrywrites: 'retryWrites',
|
||||
uuidrepresentation: 'uuidRepresentation',
|
||||
zlibcompressionlevel: 'zlibCompressionLevel'
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the value for `key`, allowing for any required translation
|
||||
*
|
||||
* @param {object} obj The object to set the key on
|
||||
* @param {string} key The key to set the value for
|
||||
* @param {*} value The value to set
|
||||
* @param {object} options The options used for option parsing
|
||||
*/
|
||||
function applyConnectionStringOption(obj, key, value, options) {
|
||||
// simple key translation
|
||||
if (key === 'journal') {
|
||||
key = 'j';
|
||||
} else if (key === 'wtimeoutms') {
|
||||
key = 'wtimeout';
|
||||
}
|
||||
|
||||
// more complicated translation
|
||||
if (BOOLEAN_OPTIONS.has(key)) {
|
||||
value = value === 'true' || value === true;
|
||||
} else if (key === 'appname') {
|
||||
value = decodeURIComponent(value);
|
||||
} else if (key === 'readconcernlevel') {
|
||||
key = 'readconcern';
|
||||
value = { level: value };
|
||||
}
|
||||
|
||||
// simple validation
|
||||
if (key === 'compressors') {
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
|
||||
if (!value.every(c => c === 'snappy' || c === 'zlib')) {
|
||||
throw new MongoParseError(
|
||||
'Value for `compressors` must be at least one of: `snappy`, `zlib`'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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`'
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'readpreference' && !ReadPreference.isValid(value)) {
|
||||
throw new MongoParseError(
|
||||
'Value for `readPreference` must be one of: `primary`, `primaryPreferred`, `secondary`, `secondaryPreferred`, `nearest`'
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'zlibcompressionlevel' && (value < -1 || value > 9)) {
|
||||
throw new MongoParseError('zlibCompressionLevel must be an integer between -1 and 9');
|
||||
}
|
||||
|
||||
// special cases
|
||||
if (key === 'compressors' || key === 'zlibcompressionlevel') {
|
||||
obj.compression = obj.compression || {};
|
||||
obj = obj.compression;
|
||||
}
|
||||
|
||||
if (key === 'authmechanismproperties') {
|
||||
if (typeof value.SERVICE_NAME === 'string') obj.gssapiServiceName = value.SERVICE_NAME;
|
||||
if (typeof value.SERVICE_REALM === 'string') obj.gssapiServiceRealm = value.SERVICE_REALM;
|
||||
if (typeof value.CANONICALIZE_HOST_NAME !== 'undefined') {
|
||||
obj.gssapiCanonicalizeHostName = value.CANONICALIZE_HOST_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
// set the actual value
|
||||
if (options.caseTranslate && CASE_TRANSLATION[key]) {
|
||||
obj[CASE_TRANSLATION[key]] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
const USERNAME_REQUIRED_MECHANISMS = new Set([
|
||||
'GSSAPI',
|
||||
'MONGODB-CR',
|
||||
'PLAIN',
|
||||
'SCRAM-SHA-1',
|
||||
'SCRAM-SHA-256'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Modifies the parsed connection string object taking into account expectations we
|
||||
* have for authentication-related options.
|
||||
*
|
||||
* @param {object} parsed The parsed connection string result
|
||||
* @return The parsed connection string result possibly modified for auth expectations
|
||||
*/
|
||||
function applyAuthExpectations(parsed) {
|
||||
if (parsed.options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = parsed.options;
|
||||
const authSource = options.authsource || options.authSource;
|
||||
if (authSource != null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: authSource });
|
||||
}
|
||||
|
||||
const authMechanism = options.authmechanism || options.authMechanism;
|
||||
if (authMechanism != null) {
|
||||
if (
|
||||
USERNAME_REQUIRED_MECHANISMS.has(authMechanism) &&
|
||||
(!parsed.auth || parsed.auth.username == null)
|
||||
) {
|
||||
throw new MongoParseError(`Username required for mechanism \`${authMechanism}\``);
|
||||
}
|
||||
|
||||
if (authMechanism === 'GSSAPI') {
|
||||
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}\``);
|
||||
}
|
||||
|
||||
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 === 'PLAIN') {
|
||||
if (parsed.auth && parsed.auth.db == null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to `admin` if nothing else was resolved
|
||||
if (parsed.auth && parsed.auth.db == null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: 'admin' });
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a query string according the connection string spec.
|
||||
*
|
||||
* @param {String} query The query string to parse
|
||||
* @param {object} [options] The options used for options parsing
|
||||
* @return {Object|Error} The parsed query string as an object, or an error if one was encountered
|
||||
*/
|
||||
function parseQueryString(query, options) {
|
||||
const result = {};
|
||||
let parsedQueryString = qs.parse(query);
|
||||
|
||||
for (const key in parsedQueryString) {
|
||||
const value = parsedQueryString[key];
|
||||
if (value === '' || value == null) {
|
||||
throw new MongoParseError('Incomplete key value pair for option');
|
||||
}
|
||||
|
||||
const normalizedKey = key.toLowerCase();
|
||||
const parsedValue = parseQueryStringItemValue(normalizedKey, value);
|
||||
applyConnectionStringOption(result, normalizedKey, parsedValue, options);
|
||||
}
|
||||
|
||||
// special cases for known deprecated options
|
||||
if (result.wtimeout && result.wtimeoutms) {
|
||||
delete result.wtimeout;
|
||||
console.warn('Unsupported option `wtimeout` specified');
|
||||
}
|
||||
|
||||
return Object.keys(result).length ? result : null;
|
||||
}
|
||||
|
||||
const PROTOCOL_MONGODB = 'mongodb';
|
||||
const PROTOCOL_MONGODB_SRV = 'mongodb+srv';
|
||||
const SUPPORTED_PROTOCOLS = [PROTOCOL_MONGODB, PROTOCOL_MONGODB_SRV];
|
||||
|
||||
/**
|
||||
* Parses a MongoDB connection string
|
||||
*
|
||||
* @param {*} uri the MongoDB connection string to parse
|
||||
* @param {object} [options] Optional settings.
|
||||
* @param {boolean} [options.caseTranslate] Whether the parser should translate options back into camelCase after normalization
|
||||
* @param {parseCallback} callback
|
||||
*/
|
||||
function parseConnectionString(uri, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({}, { caseTranslate: true }, options);
|
||||
|
||||
// Check for bad uris before we parse
|
||||
try {
|
||||
URL.parse(uri);
|
||||
} catch (e) {
|
||||
return callback(new MongoParseError('URI malformed, cannot be parsed'));
|
||||
}
|
||||
|
||||
const cap = uri.match(HOSTS_RX);
|
||||
if (!cap) {
|
||||
return callback(new MongoParseError('Invalid connection string'));
|
||||
}
|
||||
|
||||
const protocol = cap[1];
|
||||
if (SUPPORTED_PROTOCOLS.indexOf(protocol) === -1) {
|
||||
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;
|
||||
|
||||
let parsedOptions;
|
||||
try {
|
||||
parsedOptions = parseQueryString(query, options);
|
||||
} catch (parseError) {
|
||||
return callback(parseError);
|
||||
}
|
||||
|
||||
parsedOptions = Object.assign({}, parsedOptions, options);
|
||||
const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null };
|
||||
if (parsedOptions.auth) {
|
||||
// maintain support for legacy options passed into `MongoClient`
|
||||
if (parsedOptions.auth.username) auth.username = parsedOptions.auth.username;
|
||||
if (parsedOptions.auth.user) auth.username = parsedOptions.auth.user;
|
||||
if (parsedOptions.auth.password) auth.password = parsedOptions.auth.password;
|
||||
}
|
||||
|
||||
if (cap[4].split('?')[0].indexOf('@') !== -1) {
|
||||
return callback(new MongoParseError('Unescaped slash in userinfo section'));
|
||||
}
|
||||
|
||||
const authorityParts = cap[3].split('@');
|
||||
if (authorityParts.length > 2) {
|
||||
return callback(new MongoParseError('Unescaped at-sign in authority section'));
|
||||
}
|
||||
|
||||
if (authorityParts.length > 1) {
|
||||
const authParts = authorityParts.shift().split(':');
|
||||
if (authParts.length > 2) {
|
||||
return callback(new MongoParseError('Unescaped colon in authority section'));
|
||||
}
|
||||
|
||||
auth.username = qs.unescape(authParts[0]);
|
||||
auth.password = authParts[1] ? qs.unescape(authParts[1]) : null;
|
||||
}
|
||||
|
||||
let hostParsingError = null;
|
||||
const hosts = authorityParts
|
||||
.shift()
|
||||
.split(',')
|
||||
.map(host => {
|
||||
let parsedHost = URL.parse(`mongodb://${host}`);
|
||||
if (parsedHost.path === '/:') {
|
||||
hostParsingError = new MongoParseError('Double colon in host identifier');
|
||||
return null;
|
||||
}
|
||||
|
||||
// heuristically determine if we're working with a domain socket
|
||||
if (host.match(/\.sock/)) {
|
||||
parsedHost.hostname = qs.unescape(host);
|
||||
parsedHost.port = null;
|
||||
}
|
||||
|
||||
if (Number.isNaN(parsedHost.port)) {
|
||||
hostParsingError = new MongoParseError('Invalid port (non-numeric string)');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = {
|
||||
host: parsedHost.hostname,
|
||||
port: parsedHost.port ? parseInt(parsedHost.port) : 27017
|
||||
};
|
||||
|
||||
if (result.port === 0) {
|
||||
hostParsingError = new MongoParseError('Invalid port (zero) with hostname');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.port > 65535) {
|
||||
hostParsingError = new MongoParseError('Invalid port (larger than 65535) with hostname');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.port < 0) {
|
||||
hostParsingError = new MongoParseError('Invalid port (negative number)');
|
||||
return;
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.filter(host => !!host);
|
||||
|
||||
if (hostParsingError) {
|
||||
return callback(hostParsingError);
|
||||
}
|
||||
|
||||
if (hosts.length === 0 || hosts[0].host === '' || hosts[0].host === null) {
|
||||
return callback(new MongoParseError('No hostname or hostnames provided in connection string'));
|
||||
}
|
||||
|
||||
const result = {
|
||||
hosts: hosts,
|
||||
auth: auth.db || auth.username ? auth : null,
|
||||
options: Object.keys(parsedOptions).length ? parsedOptions : null
|
||||
};
|
||||
|
||||
if (result.auth && result.auth.db) {
|
||||
result.defaultDatabase = result.auth.db;
|
||||
}
|
||||
|
||||
try {
|
||||
applyAuthExpectations(result);
|
||||
} catch (authError) {
|
||||
return callback(authError);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
module.exports = parseConnectionString;
|
97
node_modules/mongodb-core/lib/utils.js
generated
vendored
Normal file
97
node_modules/mongodb-core/lib/utils.js
generated
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const requireOptional = require('require_optional');
|
||||
|
||||
/**
|
||||
* Generate a UUIDv4
|
||||
*/
|
||||
const uuidV4 = () => {
|
||||
const result = crypto.randomBytes(16);
|
||||
result[6] = (result[6] & 0x0f) | 0x40;
|
||||
result[8] = (result[8] & 0x3f) | 0x80;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the duration calculated from two high resolution timers in milliseconds
|
||||
*
|
||||
* @param {Object} started A high resolution timestamp created from `process.hrtime()`
|
||||
* @returns {Number} The duration in milliseconds
|
||||
*/
|
||||
const calculateDurationInMs = started => {
|
||||
const hrtime = process.hrtime(started);
|
||||
return (hrtime[0] * 1e9 + hrtime[1]) / 1e6;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relays events for a given listener and emitter
|
||||
*
|
||||
* @param {EventEmitter} listener the EventEmitter to listen to the events from
|
||||
* @param {EventEmitter} emitter the EventEmitter to relay the events to
|
||||
*/
|
||||
function relayEvents(listener, emitter, events) {
|
||||
events.forEach(eventName => listener.on(eventName, event => emitter.emit(eventName, event)));
|
||||
}
|
||||
|
||||
function retrieveKerberos() {
|
||||
let kerberos;
|
||||
|
||||
try {
|
||||
kerberos = requireOptional('kerberos');
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error('The `kerberos` module was not found. Please install it and try again.');
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return kerberos;
|
||||
}
|
||||
|
||||
// Throw an error if an attempt to use EJSON is made when it is not installed
|
||||
const noEJSONError = function() {
|
||||
throw new Error('The `mongodb-extjson` module was not found. Please install it and try again.');
|
||||
};
|
||||
|
||||
// Facilitate loading EJSON optionally
|
||||
function retrieveEJSON() {
|
||||
let EJSON = null;
|
||||
try {
|
||||
EJSON = requireOptional('mongodb-extjson');
|
||||
} catch (error) {} // eslint-disable-line
|
||||
if (!EJSON) {
|
||||
EJSON = {
|
||||
parse: noEJSONError,
|
||||
deserialize: noEJSONError,
|
||||
serialize: noEJSONError,
|
||||
stringify: noEJSONError,
|
||||
setBSONModule: noEJSONError,
|
||||
BSON: noEJSONError
|
||||
};
|
||||
}
|
||||
|
||||
return EJSON;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks that collation is supported by server.
|
||||
*
|
||||
* @param {Server} [server] to check against
|
||||
* @param {object} [cmd] object where collation may be specified
|
||||
* @param {function} [callback] callback function
|
||||
* @return true if server does not support collation
|
||||
*/
|
||||
function collationNotSupported(server, cmd) {
|
||||
return cmd && cmd.collation && server.ismaster && server.ismaster.maxWireVersion < 5;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
uuidV4,
|
||||
calculateDurationInMs,
|
||||
relayEvents,
|
||||
collationNotSupported,
|
||||
retrieveEJSON,
|
||||
retrieveKerberos
|
||||
};
|
267
node_modules/mongodb-core/lib/wireprotocol/2_6_support.js
generated
vendored
Normal file
267
node_modules/mongodb-core/lib/wireprotocol/2_6_support.js
generated
vendored
Normal file
@@ -0,0 +1,267 @@
|
||||
'use strict';
|
||||
|
||||
const retrieveBSON = require('../connection/utils').retrieveBSON;
|
||||
const KillCursor = require('../connection/commands').KillCursor;
|
||||
const GetMore = require('../connection/commands').GetMore;
|
||||
const Query = require('../connection/commands').Query;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const getReadPreference = require('./shared').getReadPreference;
|
||||
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
|
||||
const isMongos = require('./shared').isMongos;
|
||||
const databaseNamespace = require('./shared').databaseNamespace;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
|
||||
class WireProtocol {
|
||||
insert(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'insert', 'documents', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
update(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'update', 'updates', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
remove(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'delete', 'deletes', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
killCursor(server, ns, cursorState, callback) {
|
||||
const bson = server.s.bson;
|
||||
const pool = server.s.pool;
|
||||
const cursorId = cursorState.cursorId;
|
||||
const killCursor = new KillCursor(bson, ns, [cursorId]);
|
||||
const options = {
|
||||
immediateRelease: true,
|
||||
noResponse: true
|
||||
};
|
||||
|
||||
if (typeof cursorState.session === 'object') {
|
||||
options.session = cursorState.session;
|
||||
}
|
||||
|
||||
if (pool && pool.isConnected()) {
|
||||
try {
|
||||
pool.write(killCursor, options, callback);
|
||||
} catch (err) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(err, null);
|
||||
} else {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMore(server, ns, cursorState, batchSize, options, callback) {
|
||||
const bson = server.s.bson;
|
||||
const getMore = new GetMore(bson, ns, cursorState.cursorId, { numberToReturn: batchSize });
|
||||
function queryCallback(err, result) {
|
||||
if (err) return callback(err);
|
||||
const response = result.message;
|
||||
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if (response.cursorNotFound) {
|
||||
return callback(new MongoError('Cursor does not exist, was killed, or timed out'), null);
|
||||
}
|
||||
|
||||
const cursorId =
|
||||
typeof response.cursorId === 'number'
|
||||
? Long.fromNumber(response.cursorId)
|
||||
: response.cursorId;
|
||||
|
||||
cursorState.documents = response.documents;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
callback(null, null, response.connection);
|
||||
}
|
||||
|
||||
const queryOptions = applyCommonQueryOptions({}, cursorState);
|
||||
server.s.pool.write(getMore, queryOptions, queryCallback);
|
||||
}
|
||||
|
||||
query(server, ns, cmd, cursorState, options, callback) {
|
||||
if (cursorState.cursorId != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = setupClassicFind(server, ns, cmd, cursorState, options);
|
||||
const queryOptions = applyCommonQueryOptions({}, cursorState);
|
||||
if (typeof query.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = query.documentsReturnedIn;
|
||||
}
|
||||
|
||||
server.s.pool.write(query, queryOptions, callback);
|
||||
}
|
||||
|
||||
command(server, ns, cmd, options, callback) {
|
||||
if (cmd == null) {
|
||||
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
const bson = server.s.bson;
|
||||
const pool = server.s.pool;
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
|
||||
let finalCmd = Object.assign({}, cmd);
|
||||
if (finalCmd.readConcern) {
|
||||
if (finalCmd.readConcern.level !== 'local') {
|
||||
return callback(
|
||||
new MongoError(
|
||||
`server ${JSON.stringify(finalCmd)} command does not support a readConcern level of ${
|
||||
finalCmd.readConcern.level
|
||||
}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
delete finalCmd['readConcern'];
|
||||
}
|
||||
|
||||
if (isMongos(server) && readPreference && readPreference.preference !== 'primary') {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
command: true,
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
// This value is not overridable
|
||||
commandOptions.slaveOk = readPreference.slaveOk();
|
||||
|
||||
try {
|
||||
const query = new Query(bson, `${databaseNamespace(ns)}.$cmd`, finalCmd, commandOptions);
|
||||
pool.write(query, commandOptions, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeWrite(handler, server, type, opsField, ns, ops, options, callback) {
|
||||
if (ops.length === 0) throw new MongoError('insert must contain at least one document');
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
options = options || {};
|
||||
}
|
||||
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const writeConcern = options.writeConcern;
|
||||
|
||||
const writeCommand = {};
|
||||
writeCommand[type] = collectionNamespace(ns);
|
||||
writeCommand[opsField] = ops;
|
||||
writeCommand.ordered = ordered;
|
||||
|
||||
if (writeConcern && Object.keys(writeConcern).length > 0) {
|
||||
writeCommand.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
checkKeys: type === 'insert',
|
||||
numberToReturn: 1
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
handler.command(server, ns, writeCommand, commandOptions, callback);
|
||||
}
|
||||
|
||||
function setupClassicFind(server, ns, cmd, cursorState, options) {
|
||||
options = options || {};
|
||||
const bson = server.s.bson;
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
|
||||
let numberToReturn = 0;
|
||||
if (cursorState.limit === 0) {
|
||||
numberToReturn = cursorState.batchSize;
|
||||
} else if (
|
||||
cursorState.limit < 0 ||
|
||||
cursorState.limit < cursorState.batchSize ||
|
||||
(cursorState.limit > 0 && cursorState.batchSize === 0)
|
||||
) {
|
||||
numberToReturn = cursorState.limit;
|
||||
} else {
|
||||
numberToReturn = cursorState.batchSize;
|
||||
}
|
||||
|
||||
const numberToSkip = cursorState.skip || 0;
|
||||
|
||||
const findCmd = {};
|
||||
if (isMongos(server) && readPreference) {
|
||||
findCmd['$readPreference'] = readPreference.toJSON();
|
||||
}
|
||||
|
||||
if (cmd.sort) findCmd['$orderby'] = cmd.sort;
|
||||
if (cmd.hint) findCmd['$hint'] = cmd.hint;
|
||||
if (cmd.snapshot) findCmd['$snapshot'] = cmd.snapshot;
|
||||
if (typeof cmd.returnKey !== 'undefined') findCmd['$returnKey'] = cmd.returnKey;
|
||||
if (cmd.maxScan) findCmd['$maxScan'] = cmd.maxScan;
|
||||
if (cmd.min) findCmd['$min'] = cmd.min;
|
||||
if (cmd.max) findCmd['$max'] = cmd.max;
|
||||
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
|
||||
if (cmd.comment) findCmd['$comment'] = cmd.comment;
|
||||
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
|
||||
if (cmd.explain) {
|
||||
// nToReturn must be 0 (match all) or negative (match N and close cursor)
|
||||
// nToReturn > 0 will give explain results equivalent to limit(0)
|
||||
numberToReturn = -Math.abs(cmd.limit || 0);
|
||||
findCmd['$explain'] = true;
|
||||
}
|
||||
|
||||
findCmd['$query'] = cmd.query;
|
||||
if (cmd.readConcern && cmd.readConcern.level !== 'local') {
|
||||
throw new MongoError(
|
||||
`server find command does not support a readConcern level of ${cmd.readConcern.level}`
|
||||
);
|
||||
}
|
||||
|
||||
if (cmd.readConcern) {
|
||||
cmd = Object.assign({}, cmd);
|
||||
delete cmd['readConcern'];
|
||||
}
|
||||
|
||||
const serializeFunctions =
|
||||
typeof options.serializeFunctions === 'boolean' ? options.serializeFunctions : false;
|
||||
const ignoreUndefined =
|
||||
typeof options.ignoreUndefined === 'boolean' ? options.ignoreUndefined : false;
|
||||
|
||||
const query = new Query(bson, ns, findCmd, {
|
||||
numberToSkip: numberToSkip,
|
||||
numberToReturn: numberToReturn,
|
||||
pre32Limit: typeof cmd.limit !== 'undefined' ? cmd.limit : undefined,
|
||||
checkKeys: false,
|
||||
returnFieldSelector: cmd.fields,
|
||||
serializeFunctions: serializeFunctions,
|
||||
ignoreUndefined: ignoreUndefined
|
||||
});
|
||||
|
||||
if (typeof cmd.tailable === 'boolean') query.tailable = cmd.tailable;
|
||||
if (typeof cmd.oplogReplay === 'boolean') query.oplogReplay = cmd.oplogReplay;
|
||||
if (typeof cmd.noCursorTimeout === 'boolean') query.noCursorTimeout = cmd.noCursorTimeout;
|
||||
if (typeof cmd.awaitData === 'boolean') query.awaitData = cmd.awaitData;
|
||||
if (typeof cmd.partial === 'boolean') query.partial = cmd.partial;
|
||||
|
||||
query.slaveOk = readPreference.slaveOk();
|
||||
return query;
|
||||
}
|
||||
|
||||
module.exports = WireProtocol;
|
397
node_modules/mongodb-core/lib/wireprotocol/3_2_support.js
generated
vendored
Normal file
397
node_modules/mongodb-core/lib/wireprotocol/3_2_support.js
generated
vendored
Normal file
@@ -0,0 +1,397 @@
|
||||
'use strict';
|
||||
|
||||
const Query = require('../connection/commands').Query;
|
||||
const retrieveBSON = require('../connection/utils').retrieveBSON;
|
||||
const MongoError = require('../error').MongoError;
|
||||
const MongoNetworkError = require('../error').MongoNetworkError;
|
||||
const getReadPreference = require('./shared').getReadPreference;
|
||||
const BSON = retrieveBSON();
|
||||
const Long = BSON.Long;
|
||||
const ReadPreference = require('../topologies/read_preference');
|
||||
const TxnState = require('../transactions').TxnState;
|
||||
const isMongos = require('./shared').isMongos;
|
||||
const databaseNamespace = require('./shared').databaseNamespace;
|
||||
const collectionNamespace = require('./shared').collectionNamespace;
|
||||
|
||||
class WireProtocol {
|
||||
insert(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'insert', 'documents', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
update(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'update', 'updates', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
remove(server, ns, ops, options, callback) {
|
||||
executeWrite(this, server, 'delete', 'deletes', ns, ops, options, callback);
|
||||
}
|
||||
|
||||
killCursor(server, ns, cursorState, callback) {
|
||||
callback = typeof callback === 'function' ? callback : () => {};
|
||||
const cursorId = cursorState.cursorId;
|
||||
const killCursorCmd = {
|
||||
killCursors: collectionNamespace(ns),
|
||||
cursors: [cursorId]
|
||||
};
|
||||
|
||||
const options = {};
|
||||
if (typeof cursorState.session === 'object') options.session = cursorState.session;
|
||||
|
||||
this.command(server, ns, killCursorCmd, options, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const response = result.message;
|
||||
if (response.cursorNotFound) {
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.documents) || response.documents.length === 0) {
|
||||
return callback(
|
||||
new MongoError(`invalid killCursors result returned for cursor id ${cursorId}`)
|
||||
);
|
||||
}
|
||||
|
||||
callback(null, response.documents[0]);
|
||||
});
|
||||
}
|
||||
|
||||
getMore(server, ns, cursorState, batchSize, options, callback) {
|
||||
options = options || {};
|
||||
const getMoreCmd = {
|
||||
getMore: cursorState.cursorId,
|
||||
collection: collectionNamespace(ns),
|
||||
batchSize: Math.abs(batchSize)
|
||||
};
|
||||
|
||||
if (cursorState.cmd.tailable && typeof cursorState.cmd.maxAwaitTimeMS === 'number') {
|
||||
getMoreCmd.maxTimeMS = cursorState.cmd.maxAwaitTimeMS;
|
||||
}
|
||||
|
||||
function queryCallback(err, result) {
|
||||
if (err) return callback(err);
|
||||
const response = result.message;
|
||||
|
||||
// If we have a timed out query or a cursor that was killed
|
||||
if (response.cursorNotFound) {
|
||||
return callback(new MongoNetworkError('cursor killed or timed out'), null);
|
||||
}
|
||||
|
||||
// Raw, return all the extracted documents
|
||||
if (cursorState.raw) {
|
||||
cursorState.documents = response.documents;
|
||||
cursorState.cursorId = response.cursorId;
|
||||
return callback(null, response.documents);
|
||||
}
|
||||
|
||||
// We have an error detected
|
||||
if (response.documents[0].ok === 0) {
|
||||
return callback(new MongoError(response.documents[0]));
|
||||
}
|
||||
|
||||
// Ensure we have a Long valid cursor id
|
||||
const cursorId =
|
||||
typeof response.documents[0].cursor.id === 'number'
|
||||
? Long.fromNumber(response.documents[0].cursor.id)
|
||||
: response.documents[0].cursor.id;
|
||||
|
||||
cursorState.documents = response.documents[0].cursor.nextBatch;
|
||||
cursorState.cursorId = cursorId;
|
||||
|
||||
callback(null, response.documents[0], response.connection);
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
returnFieldSelector: null,
|
||||
documentsReturnedIn: 'nextBatch'
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
this.command(server, ns, getMoreCmd, commandOptions, queryCallback);
|
||||
}
|
||||
|
||||
query(server, ns, cmd, cursorState, options, callback) {
|
||||
options = options || {};
|
||||
if (cursorState.cursorId != null) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (cmd == null) {
|
||||
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
|
||||
}
|
||||
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
|
||||
|
||||
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
|
||||
// side-effect. Change this ASAP
|
||||
cmd.virtual = false;
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
documentsReturnedIn: 'firstBatch',
|
||||
numberToReturn: 1,
|
||||
slaveOk: readPreference.slaveOk()
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
if (cmd.readPreference) commandOptions.readPreference = readPreference;
|
||||
this.command(server, ns, findCmd, commandOptions, callback);
|
||||
}
|
||||
|
||||
command(server, ns, cmd, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = options || {};
|
||||
|
||||
if (cmd == null) {
|
||||
return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
|
||||
}
|
||||
|
||||
const bson = server.s.bson;
|
||||
const pool = server.s.pool;
|
||||
const readPreference = getReadPreference(cmd, options);
|
||||
|
||||
let finalCmd = Object.assign({}, cmd);
|
||||
if (isMongos(server) && readPreference && readPreference.preference !== 'primary') {
|
||||
finalCmd = {
|
||||
$query: finalCmd,
|
||||
$readPreference: readPreference.toJSON()
|
||||
};
|
||||
}
|
||||
|
||||
const err = decorateWithSessionsData(finalCmd, options.session, options);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
command: true,
|
||||
numberToSkip: 0,
|
||||
numberToReturn: -1,
|
||||
checkKeys: false
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
// This value is not overridable
|
||||
commandOptions.slaveOk = readPreference.slaveOk();
|
||||
|
||||
try {
|
||||
const query = new Query(bson, `${databaseNamespace(ns)}.$cmd`, finalCmd, commandOptions);
|
||||
pool.write(query, commandOptions, callback);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTransactionCommand(command) {
|
||||
return !!(command.commitTransaction || command.abortTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally decorate a command with sessions specific keys
|
||||
*
|
||||
* @param {Object} command the command to decorate
|
||||
* @param {ClientSession} session the session tracking transaction state
|
||||
* @param {Object} [options] Optional settings passed to calling operation
|
||||
* @param {Function} [callback] Optional callback passed from calling operation
|
||||
* @return {MongoError|null} An error, if some error condition was met
|
||||
*/
|
||||
function decorateWithSessionsData(command, session, options) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
// first apply non-transaction-specific sessions data
|
||||
const serverSession = session.serverSession;
|
||||
const inTransaction = session.inTransaction() || isTransactionCommand(command);
|
||||
const isRetryableWrite = options.willRetryWrite;
|
||||
|
||||
if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
|
||||
command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
|
||||
}
|
||||
|
||||
// now attempt to apply transaction-specific sessions data
|
||||
if (!inTransaction) {
|
||||
if (session.transaction.state !== TxnState.NO_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.NO_TRANSACTION);
|
||||
}
|
||||
|
||||
// for causal consistency
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.readPreference && !options.readPreference.equals(ReadPreference.primary)) {
|
||||
return new MongoError(
|
||||
`Read preference in a transaction must be primary, not: ${options.readPreference.mode}`
|
||||
);
|
||||
}
|
||||
|
||||
// `autocommit` must always be false to differentiate from retryable writes
|
||||
command.autocommit = false;
|
||||
|
||||
if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
|
||||
session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
|
||||
command.startTransaction = true;
|
||||
|
||||
const readConcern =
|
||||
session.transaction.options.readConcern || session.clientOptions.readConcern;
|
||||
if (readConcern) {
|
||||
command.readConcern = readConcern;
|
||||
}
|
||||
|
||||
if (session.supports.causalConsistency && session.operationTime) {
|
||||
command.readConcern = command.readConcern || {};
|
||||
Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeWrite(handler, server, type, opsField, ns, ops, options, callback) {
|
||||
if (ops.length === 0) throw new MongoError('insert must contain at least one document');
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
options = options || {};
|
||||
}
|
||||
|
||||
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
|
||||
const writeConcern = options.writeConcern;
|
||||
|
||||
const writeCommand = {};
|
||||
writeCommand[type] = collectionNamespace(ns);
|
||||
writeCommand[opsField] = ops;
|
||||
writeCommand.ordered = ordered;
|
||||
|
||||
if (writeConcern && Object.keys(writeConcern).length > 0) {
|
||||
writeCommand.writeConcern = writeConcern;
|
||||
}
|
||||
|
||||
if (options.collation) {
|
||||
for (let i = 0; i < writeCommand[opsField].length; i++) {
|
||||
if (!writeCommand[opsField][i].collation) {
|
||||
writeCommand[opsField][i].collation = options.collation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bypassDocumentValidation === true) {
|
||||
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
|
||||
}
|
||||
|
||||
const commandOptions = Object.assign(
|
||||
{
|
||||
checkKeys: type === 'insert',
|
||||
numberToReturn: 1
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
handler.command(server, ns, writeCommand, commandOptions, callback);
|
||||
}
|
||||
|
||||
function prepareFindCommand(server, ns, cmd, cursorState) {
|
||||
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
|
||||
let findCmd = {
|
||||
find: collectionNamespace(ns)
|
||||
};
|
||||
|
||||
if (cmd.query) {
|
||||
if (cmd.query['$query']) {
|
||||
findCmd.filter = cmd.query['$query'];
|
||||
} else {
|
||||
findCmd.filter = cmd.query;
|
||||
}
|
||||
}
|
||||
|
||||
let sortValue = cmd.sort;
|
||||
if (Array.isArray(sortValue)) {
|
||||
const sortObject = {};
|
||||
|
||||
if (sortValue.length > 0 && !Array.isArray(sortValue[0])) {
|
||||
let sortDirection = sortValue[1];
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
sortObject[sortValue[0]] = sortDirection;
|
||||
} else {
|
||||
for (let i = 0; i < sortValue.length; i++) {
|
||||
let sortDirection = sortValue[i][1];
|
||||
if (sortDirection === 'asc') {
|
||||
sortDirection = 1;
|
||||
} else if (sortDirection === 'desc') {
|
||||
sortDirection = -1;
|
||||
}
|
||||
|
||||
sortObject[sortValue[i][0]] = sortDirection;
|
||||
}
|
||||
}
|
||||
|
||||
sortValue = sortObject;
|
||||
}
|
||||
|
||||
if (cmd.sort) findCmd.sort = sortValue;
|
||||
if (cmd.fields) findCmd.projection = cmd.fields;
|
||||
if (cmd.hint) findCmd.hint = cmd.hint;
|
||||
if (cmd.skip) findCmd.skip = cmd.skip;
|
||||
if (cmd.limit) findCmd.limit = cmd.limit;
|
||||
if (cmd.limit < 0) {
|
||||
findCmd.limit = Math.abs(cmd.limit);
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
if (typeof cmd.batchSize === 'number') {
|
||||
if (cmd.batchSize < 0) {
|
||||
if (cmd.limit !== 0 && Math.abs(cmd.batchSize) < Math.abs(cmd.limit)) {
|
||||
findCmd.limit = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
findCmd.singleBatch = true;
|
||||
}
|
||||
|
||||
findCmd.batchSize = Math.abs(cmd.batchSize);
|
||||
}
|
||||
|
||||
if (cmd.comment) findCmd.comment = cmd.comment;
|
||||
if (cmd.maxScan) findCmd.maxScan = cmd.maxScan;
|
||||
if (cmd.maxTimeMS) findCmd.maxTimeMS = cmd.maxTimeMS;
|
||||
if (cmd.min) findCmd.min = cmd.min;
|
||||
if (cmd.max) findCmd.max = cmd.max;
|
||||
findCmd.returnKey = cmd.returnKey ? cmd.returnKey : false;
|
||||
findCmd.showRecordId = cmd.showDiskLoc ? cmd.showDiskLoc : false;
|
||||
if (cmd.snapshot) findCmd.snapshot = cmd.snapshot;
|
||||
if (cmd.tailable) findCmd.tailable = cmd.tailable;
|
||||
if (cmd.oplogReplay) findCmd.oplogReplay = cmd.oplogReplay;
|
||||
if (cmd.noCursorTimeout) findCmd.noCursorTimeout = cmd.noCursorTimeout;
|
||||
if (cmd.awaitData) findCmd.awaitData = cmd.awaitData;
|
||||
if (cmd.awaitdata) findCmd.awaitData = cmd.awaitdata;
|
||||
if (cmd.partial) findCmd.partial = cmd.partial;
|
||||
if (cmd.collation) findCmd.collation = cmd.collation;
|
||||
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
|
||||
|
||||
// If we have explain, we need to rewrite the find command
|
||||
// to wrap it in the explain command
|
||||
if (cmd.explain) {
|
||||
findCmd = {
|
||||
explain: findCmd
|
||||
};
|
||||
}
|
||||
|
||||
return findCmd;
|
||||
}
|
||||
|
||||
module.exports = WireProtocol;
|
73
node_modules/mongodb-core/lib/wireprotocol/compression.js
generated
vendored
Normal file
73
node_modules/mongodb-core/lib/wireprotocol/compression.js
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
var Snappy = require('../connection/utils').retrieveSnappy(),
|
||||
zlib = require('zlib');
|
||||
|
||||
var compressorIDs = {
|
||||
snappy: 1,
|
||||
zlib: 2
|
||||
};
|
||||
|
||||
var uncompressibleCommands = [
|
||||
'ismaster',
|
||||
'saslStart',
|
||||
'saslContinue',
|
||||
'getnonce',
|
||||
'authenticate',
|
||||
'createUser',
|
||||
'updateUser',
|
||||
'copydbSaslStart',
|
||||
'copydbgetnonce',
|
||||
'copydb'
|
||||
];
|
||||
|
||||
// Facilitate compressing a message using an agreed compressor
|
||||
var compress = function(self, dataToBeCompressed, callback) {
|
||||
switch (self.options.agreedCompressor) {
|
||||
case 'snappy':
|
||||
Snappy.compress(dataToBeCompressed, callback);
|
||||
break;
|
||||
case 'zlib':
|
||||
// Determine zlibCompressionLevel
|
||||
var zlibOptions = {};
|
||||
if (self.options.zlibCompressionLevel) {
|
||||
zlibOptions.level = self.options.zlibCompressionLevel;
|
||||
}
|
||||
zlib.deflate(dataToBeCompressed, zlibOptions, callback);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
'Attempt to compress message using unknown compressor "' +
|
||||
self.options.agreedCompressor +
|
||||
'".'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Decompress a message using the given compressor
|
||||
var decompress = function(compressorID, compressedData, callback) {
|
||||
if (compressorID < 0 || compressorID > compressorIDs.length) {
|
||||
throw new Error(
|
||||
'Server sent message compressed using an unsupported compressor. (Received compressor ID ' +
|
||||
compressorID +
|
||||
')'
|
||||
);
|
||||
}
|
||||
switch (compressorID) {
|
||||
case compressorIDs.snappy:
|
||||
Snappy.uncompress(compressedData, callback);
|
||||
break;
|
||||
case compressorIDs.zlib:
|
||||
zlib.inflate(compressedData, callback);
|
||||
break;
|
||||
default:
|
||||
callback(null, compressedData);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
compressorIDs: compressorIDs,
|
||||
uncompressibleCommands: uncompressibleCommands,
|
||||
compress: compress,
|
||||
decompress: decompress
|
||||
};
|
101
node_modules/mongodb-core/lib/wireprotocol/shared.js
generated
vendored
Normal file
101
node_modules/mongodb-core/lib/wireprotocol/shared.js
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
var ReadPreference = require('../topologies/read_preference'),
|
||||
MongoError = require('../error').MongoError;
|
||||
|
||||
var MESSAGE_HEADER_SIZE = 16;
|
||||
|
||||
// OPCODE Numbers
|
||||
// Defined at https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#request-opcodes
|
||||
var opcodes = {
|
||||
OP_REPLY: 1,
|
||||
OP_UPDATE: 2001,
|
||||
OP_INSERT: 2002,
|
||||
OP_QUERY: 2004,
|
||||
OP_GETMORE: 2005,
|
||||
OP_DELETE: 2006,
|
||||
OP_KILL_CURSORS: 2007,
|
||||
OP_COMPRESSED: 2012
|
||||
};
|
||||
|
||||
var getReadPreference = function(cmd, options) {
|
||||
// Default to command version of the readPreference
|
||||
var readPreference = cmd.readPreference || new ReadPreference('primary');
|
||||
// If we have an option readPreference override the command one
|
||||
if (options.readPreference) {
|
||||
readPreference = options.readPreference;
|
||||
}
|
||||
|
||||
if (typeof readPreference === 'string') {
|
||||
readPreference = new ReadPreference(readPreference);
|
||||
}
|
||||
|
||||
if (!(readPreference instanceof ReadPreference)) {
|
||||
throw new MongoError('read preference must be a ReadPreference instance');
|
||||
}
|
||||
|
||||
return readPreference;
|
||||
};
|
||||
|
||||
// Parses the header of a wire protocol message
|
||||
var parseHeader = function(message) {
|
||||
return {
|
||||
length: message.readInt32LE(0),
|
||||
requestId: message.readInt32LE(4),
|
||||
responseTo: message.readInt32LE(8),
|
||||
opCode: message.readInt32LE(12)
|
||||
};
|
||||
};
|
||||
|
||||
function applyCommonQueryOptions(queryOptions, options) {
|
||||
Object.assign(queryOptions, {
|
||||
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
||||
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
||||
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
||||
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
||||
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
||||
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false
|
||||
});
|
||||
|
||||
if (typeof options.socketTimeout === 'number') {
|
||||
queryOptions.socketTimeout = options.socketTimeout;
|
||||
}
|
||||
|
||||
if (options.session) {
|
||||
queryOptions.session = options.session;
|
||||
}
|
||||
|
||||
if (typeof options.documentsReturnedIn === 'string') {
|
||||
queryOptions.documentsReturnedIn = options.documentsReturnedIn;
|
||||
}
|
||||
|
||||
return queryOptions;
|
||||
}
|
||||
|
||||
function isMongos(server) {
|
||||
if (server.type === 'mongos') return true;
|
||||
if (server.parent && server.parent.type === 'mongos') return true;
|
||||
// NOTE: handle unified topology
|
||||
return false;
|
||||
}
|
||||
|
||||
function databaseNamespace(ns) {
|
||||
return ns.split('.')[0];
|
||||
}
|
||||
function collectionNamespace(ns) {
|
||||
return ns
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getReadPreference,
|
||||
MESSAGE_HEADER_SIZE,
|
||||
opcodes,
|
||||
parseHeader,
|
||||
applyCommonQueryOptions,
|
||||
isMongos,
|
||||
databaseNamespace,
|
||||
collectionNamespace
|
||||
};
|
Reference in New Issue
Block a user