You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					998 lines
				
				27 KiB
			
		
		
			
		
	
	
					998 lines
				
				27 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | const MongoError = require('./core/error').MongoError; | ||
|  | const WriteConcern = require('./write_concern'); | ||
|  | 
 | ||
|  | var shallowClone = function(obj) { | ||
|  |   var copy = {}; | ||
|  |   for (var name in obj) copy[name] = obj[name]; | ||
|  |   return copy; | ||
|  | }; | ||
|  | 
 | ||
|  | // Set simple property
 | ||
|  | var getSingleProperty = function(obj, name, value) { | ||
|  |   Object.defineProperty(obj, name, { | ||
|  |     enumerable: true, | ||
|  |     get: function() { | ||
|  |       return value; | ||
|  |     } | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | var formatSortValue = (exports.formatSortValue = function(sortDirection) { | ||
|  |   var value = ('' + sortDirection).toLowerCase(); | ||
|  | 
 | ||
|  |   switch (value) { | ||
|  |     case 'ascending': | ||
|  |     case 'asc': | ||
|  |     case '1': | ||
|  |       return 1; | ||
|  |     case 'descending': | ||
|  |     case 'desc': | ||
|  |     case '-1': | ||
|  |       return -1; | ||
|  |     default: | ||
|  |       throw new Error( | ||
|  |         'Illegal sort clause, must be of the form ' + | ||
|  |           "[['field1', '(ascending|descending)'], " + | ||
|  |           "['field2', '(ascending|descending)']]" | ||
|  |       ); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | var formattedOrderClause = (exports.formattedOrderClause = function(sortValue) { | ||
|  |   var orderBy = new Map(); | ||
|  |   if (sortValue == null) return null; | ||
|  |   if (Array.isArray(sortValue)) { | ||
|  |     if (sortValue.length === 0) { | ||
|  |       return null; | ||
|  |     } | ||
|  | 
 | ||
|  |     for (var i = 0; i < sortValue.length; i++) { | ||
|  |       if (sortValue[i].constructor === String) { | ||
|  |         orderBy.set(`${sortValue[i]}`, 1); | ||
|  |       } else { | ||
|  |         orderBy.set(`${sortValue[i][0]}`, formatSortValue(sortValue[i][1])); | ||
|  |       } | ||
|  |     } | ||
|  |   } else if (sortValue != null && typeof sortValue === 'object') { | ||
|  |     if (sortValue instanceof Map) { | ||
|  |       orderBy = sortValue; | ||
|  |     } else { | ||
|  |       var sortKeys = Object.keys(sortValue); | ||
|  |       for (var k of sortKeys) { | ||
|  |         orderBy.set(k, sortValue[k]); | ||
|  |       } | ||
|  |     } | ||
|  |   } else if (typeof sortValue === 'string') { | ||
|  |     orderBy.set(`${sortValue}`, 1); | ||
|  |   } else { | ||
|  |     throw new Error( | ||
|  |       'Illegal sort clause, must be of the form ' + | ||
|  |         "[['field1', '(ascending|descending)'], ['field2', '(ascending|descending)']]" | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   return orderBy; | ||
|  | }); | ||
|  | 
 | ||
|  | var checkCollectionName = function checkCollectionName(collectionName) { | ||
|  |   if ('string' !== typeof collectionName) { | ||
|  |     throw new MongoError('collection name must be a String'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!collectionName || collectionName.indexOf('..') !== -1) { | ||
|  |     throw new MongoError('collection names cannot be empty'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if ( | ||
|  |     collectionName.indexOf('$') !== -1 && | ||
|  |     collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null | ||
|  |   ) { | ||
|  |     throw new MongoError("collection names must not contain '$'"); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (collectionName.match(/^\.|\.$/) != null) { | ||
|  |     throw new MongoError("collection names must not start or end with '.'"); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Validate that we are not passing 0x00 in the collection name
 | ||
|  |   if (collectionName.indexOf('\x00') !== -1) { | ||
|  |     throw new MongoError('collection names cannot contain a null character'); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | var handleCallback = function(callback, err, value1, value2) { | ||
|  |   try { | ||
|  |     if (callback == null) return; | ||
|  | 
 | ||
|  |     if (callback) { | ||
|  |       return value2 ? callback(err, value1, value2) : callback(err, value1); | ||
|  |     } | ||
|  |   } catch (err) { | ||
|  |     process.nextTick(function() { | ||
|  |       throw err; | ||
|  |     }); | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Wrap a Mongo error document in an Error instance | ||
|  |  * @ignore | ||
|  |  * @api private | ||
|  |  */ | ||
|  | var toError = function(error) { | ||
|  |   if (error instanceof Error) return error; | ||
|  | 
 | ||
|  |   var msg = error.err || error.errmsg || error.errMessage || error; | ||
|  |   var e = MongoError.create({ message: msg, driver: true }); | ||
|  | 
 | ||
|  |   // Get all object keys
 | ||
|  |   var keys = typeof error === 'object' ? Object.keys(error) : []; | ||
|  | 
 | ||
|  |   for (var i = 0; i < keys.length; i++) { | ||
|  |     try { | ||
|  |       e[keys[i]] = error[keys[i]]; | ||
|  |     } catch (err) { | ||
|  |       // continue
 | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return e; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @ignore | ||
|  |  */ | ||
|  | var normalizeHintField = function normalizeHintField(hint) { | ||
|  |   var finalHint = null; | ||
|  | 
 | ||
|  |   if (typeof hint === 'string') { | ||
|  |     finalHint = hint; | ||
|  |   } else if (Array.isArray(hint)) { | ||
|  |     finalHint = {}; | ||
|  | 
 | ||
|  |     hint.forEach(function(param) { | ||
|  |       finalHint[param] = 1; | ||
|  |     }); | ||
|  |   } else if (hint != null && typeof hint === 'object') { | ||
|  |     finalHint = {}; | ||
|  |     for (var name in hint) { | ||
|  |       finalHint[name] = hint[name]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return finalHint; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create index name based on field spec | ||
|  |  * | ||
|  |  * @ignore | ||
|  |  * @api private | ||
|  |  */ | ||
|  | var parseIndexOptions = function(fieldOrSpec) { | ||
|  |   var fieldHash = {}; | ||
|  |   var indexes = []; | ||
|  |   var keys; | ||
|  | 
 | ||
|  |   // Get all the fields accordingly
 | ||
|  |   if ('string' === typeof fieldOrSpec) { | ||
|  |     // 'type'
 | ||
|  |     indexes.push(fieldOrSpec + '_' + 1); | ||
|  |     fieldHash[fieldOrSpec] = 1; | ||
|  |   } else if (Array.isArray(fieldOrSpec)) { | ||
|  |     fieldOrSpec.forEach(function(f) { | ||
|  |       if ('string' === typeof f) { | ||
|  |         // [{location:'2d'}, 'type']
 | ||
|  |         indexes.push(f + '_' + 1); | ||
|  |         fieldHash[f] = 1; | ||
|  |       } else if (Array.isArray(f)) { | ||
|  |         // [['location', '2d'],['type', 1]]
 | ||
|  |         indexes.push(f[0] + '_' + (f[1] || 1)); | ||
|  |         fieldHash[f[0]] = f[1] || 1; | ||
|  |       } else if (isObject(f)) { | ||
|  |         // [{location:'2d'}, {type:1}]
 | ||
|  |         keys = Object.keys(f); | ||
|  |         keys.forEach(function(k) { | ||
|  |           indexes.push(k + '_' + f[k]); | ||
|  |           fieldHash[k] = f[k]; | ||
|  |         }); | ||
|  |       } else { | ||
|  |         // undefined (ignore)
 | ||
|  |       } | ||
|  |     }); | ||
|  |   } else if (isObject(fieldOrSpec)) { | ||
|  |     // {location:'2d', type:1}
 | ||
|  |     keys = Object.keys(fieldOrSpec); | ||
|  |     keys.forEach(function(key) { | ||
|  |       indexes.push(key + '_' + fieldOrSpec[key]); | ||
|  |       fieldHash[key] = fieldOrSpec[key]; | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   return { | ||
|  |     name: indexes.join('_'), | ||
|  |     keys: keys, | ||
|  |     fieldHash: fieldHash | ||
|  |   }; | ||
|  | }; | ||
|  | 
 | ||
|  | var isObject = (exports.isObject = function(arg) { | ||
|  |   return '[object Object]' === Object.prototype.toString.call(arg); | ||
|  | }); | ||
|  | 
 | ||
|  | var debugOptions = function(debugFields, options) { | ||
|  |   var finaloptions = {}; | ||
|  |   debugFields.forEach(function(n) { | ||
|  |     finaloptions[n] = options[n]; | ||
|  |   }); | ||
|  | 
 | ||
|  |   return finaloptions; | ||
|  | }; | ||
|  | 
 | ||
|  | var decorateCommand = function(command, options, exclude) { | ||
|  |   for (var name in options) { | ||
|  |     if (exclude.indexOf(name) === -1) command[name] = options[name]; | ||
|  |   } | ||
|  | 
 | ||
|  |   return command; | ||
|  | }; | ||
|  | 
 | ||
|  | var mergeOptions = function(target, source) { | ||
|  |   for (var name in source) { | ||
|  |     target[name] = source[name]; | ||
|  |   } | ||
|  | 
 | ||
|  |   return target; | ||
|  | }; | ||
|  | 
 | ||
|  | // Merge options with translation
 | ||
|  | var translateOptions = function(target, source) { | ||
|  |   var translations = { | ||
|  |     // SSL translation options
 | ||
|  |     sslCA: 'ca', | ||
|  |     sslCRL: 'crl', | ||
|  |     sslValidate: 'rejectUnauthorized', | ||
|  |     sslKey: 'key', | ||
|  |     sslCert: 'cert', | ||
|  |     sslPass: 'passphrase', | ||
|  |     // SocketTimeout translation options
 | ||
|  |     socketTimeoutMS: 'socketTimeout', | ||
|  |     connectTimeoutMS: 'connectionTimeout', | ||
|  |     // Replicaset options
 | ||
|  |     replicaSet: 'setName', | ||
|  |     rs_name: 'setName', | ||
|  |     secondaryAcceptableLatencyMS: 'acceptableLatency', | ||
|  |     connectWithNoPrimary: 'secondaryOnlyConnectionAllowed', | ||
|  |     // Mongos options
 | ||
|  |     acceptableLatencyMS: 'localThresholdMS' | ||
|  |   }; | ||
|  | 
 | ||
|  |   for (var name in source) { | ||
|  |     if (translations[name]) { | ||
|  |       target[translations[name]] = source[name]; | ||
|  |     } else { | ||
|  |       target[name] = source[name]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return target; | ||
|  | }; | ||
|  | 
 | ||
|  | var filterOptions = function(options, names) { | ||
|  |   var filterOptions = {}; | ||
|  | 
 | ||
|  |   for (var name in options) { | ||
|  |     if (names.indexOf(name) !== -1) filterOptions[name] = options[name]; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Filtered options
 | ||
|  |   return filterOptions; | ||
|  | }; | ||
|  | 
 | ||
|  | // Write concern keys
 | ||
|  | const WRITE_CONCERN_KEYS = ['w', 'j', 'wtimeout', 'fsync', 'writeConcern']; | ||
|  | 
 | ||
|  | /** | ||
|  |  * If there is no WriteConcern related options defined on target then inherit from source. | ||
|  |  * Otherwise, do not inherit **any** options from source. | ||
|  |  * @internal | ||
|  |  * @param {object} target - options object conditionally receiving the writeConcern options | ||
|  |  * @param {object} source - options object containing the potentially inherited writeConcern options | ||
|  |  */ | ||
|  | function conditionallyMergeWriteConcern(target, source) { | ||
|  |   let found = false; | ||
|  |   for (const wcKey of WRITE_CONCERN_KEYS) { | ||
|  |     if (wcKey in target) { | ||
|  |       // Found a writeConcern option
 | ||
|  |       found = true; | ||
|  |       break; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!found) { | ||
|  |     for (const wcKey of WRITE_CONCERN_KEYS) { | ||
|  |       if (source[wcKey]) { | ||
|  |         if (!('writeConcern' in target)) { | ||
|  |           target.writeConcern = {}; | ||
|  |         } | ||
|  |         target.writeConcern[wcKey] = source[wcKey]; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return target; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Executes the given operation with provided arguments. | ||
|  |  * | ||
|  |  * This method reduces large amounts of duplication in the entire codebase by providing | ||
|  |  * a single point for determining whether callbacks or promises should be used. Additionally | ||
|  |  * it allows for a single point of entry to provide features such as implicit sessions, which | ||
|  |  * are required by the Driver Sessions specification in the event that a ClientSession is | ||
|  |  * not provided | ||
|  |  * | ||
|  |  * @param {object} topology The topology to execute this operation on | ||
|  |  * @param {function} operation The operation to execute | ||
|  |  * @param {array} args Arguments to apply the provided operation | ||
|  |  * @param {object} [options] Options that modify the behavior of the method | ||
|  |  */ | ||
|  | const executeLegacyOperation = (topology, operation, args, options) => { | ||
|  |   if (topology == null) { | ||
|  |     throw new TypeError('This method requires a valid topology instance'); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!Array.isArray(args)) { | ||
|  |     throw new TypeError('This method requires an array of arguments to apply'); | ||
|  |   } | ||
|  | 
 | ||
|  |   options = options || {}; | ||
|  |   const Promise = topology.s.promiseLibrary; | ||
|  |   let callback = args[args.length - 1]; | ||
|  | 
 | ||
|  |   // The driver sessions spec mandates that we implicitly create sessions for operations
 | ||
|  |   // that are not explicitly provided with a session.
 | ||
|  |   let session, opOptions, owner; | ||
|  |   if (!options.skipSessions && topology.hasSessionSupport()) { | ||
|  |     opOptions = args[args.length - 2]; | ||
|  |     if (opOptions == null || opOptions.session == null) { | ||
|  |       owner = Symbol(); | ||
|  |       session = topology.startSession({ owner }); | ||
|  |       const optionsIndex = args.length - 2; | ||
|  |       args[optionsIndex] = Object.assign({}, args[optionsIndex], { session: session }); | ||
|  |     } else if (opOptions.session && opOptions.session.hasEnded) { | ||
|  |       throw new MongoError('Use of expired sessions is not permitted'); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   const makeExecuteCallback = (resolve, reject) => | ||
|  |     function executeCallback(err, result) { | ||
|  |       if (session && session.owner === owner && !options.returnsCursor) { | ||
|  |         session.endSession(() => { | ||
|  |           delete opOptions.session; | ||
|  |           if (err) return reject(err); | ||
|  |           resolve(result); | ||
|  |         }); | ||
|  |       } else { | ||
|  |         if (err) return reject(err); | ||
|  |         resolve(result); | ||
|  |       } | ||
|  |     }; | ||
|  | 
 | ||
|  |   // Execute using callback
 | ||
|  |   if (typeof callback === 'function') { | ||
|  |     callback = args.pop(); | ||
|  |     const handler = makeExecuteCallback( | ||
|  |       result => callback(null, result), | ||
|  |       err => callback(err, null) | ||
|  |     ); | ||
|  |     args.push(handler); | ||
|  | 
 | ||
|  |     try { | ||
|  |       return operation.apply(null, args); | ||
|  |     } catch (e) { | ||
|  |       handler(e); | ||
|  |       throw e; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Return a Promise
 | ||
|  |   if (args[args.length - 1] != null) { | ||
|  |     throw new TypeError('final argument to `executeLegacyOperation` must be a callback'); | ||
|  |   } | ||
|  | 
 | ||
|  |   return new Promise(function(resolve, reject) { | ||
|  |     const handler = makeExecuteCallback(resolve, reject); | ||
|  |     args[args.length - 1] = handler; | ||
|  | 
 | ||
|  |     try { | ||
|  |       return operation.apply(null, args); | ||
|  |     } catch (e) { | ||
|  |       handler(e); | ||
|  |     } | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Applies retryWrites: true to a command if retryWrites is set on the command's database. | ||
|  |  * | ||
|  |  * @param {object} target The target command to which we will apply retryWrites. | ||
|  |  * @param {object} db The database from which we can inherit a retryWrites value. | ||
|  |  */ | ||
|  | function applyRetryableWrites(target, db) { | ||
|  |   if (db && db.s.options.retryWrites) { | ||
|  |     target.retryWrites = true; | ||
|  |   } | ||
|  | 
 | ||
|  |   return target; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Applies a write concern to a command based on well defined inheritance rules, optionally | ||
|  |  * detecting support for the write concern in the first place. | ||
|  |  * | ||
|  |  * @param {Object} target the target command we will be applying the write concern to | ||
|  |  * @param {Object} sources sources where we can inherit default write concerns from | ||
|  |  * @param {Object} [options] optional settings passed into a command for write concern overrides | ||
|  |  * @returns {Object} the (now) decorated target | ||
|  |  */ | ||
|  | function applyWriteConcern(target, sources, options) { | ||
|  |   options = options || {}; | ||
|  |   const db = sources.db; | ||
|  |   const coll = sources.collection; | ||
|  | 
 | ||
|  |   if (options.session && options.session.inTransaction()) { | ||
|  |     // writeConcern is not allowed within a multi-statement transaction
 | ||
|  |     if (target.writeConcern) { | ||
|  |       delete target.writeConcern; | ||
|  |     } | ||
|  | 
 | ||
|  |     return target; | ||
|  |   } | ||
|  | 
 | ||
|  |   const writeConcern = WriteConcern.fromOptions(options); | ||
|  |   if (writeConcern) { | ||
|  |     return Object.assign(target, { writeConcern }); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (coll && coll.writeConcern) { | ||
|  |     return Object.assign(target, { writeConcern: Object.assign({}, coll.writeConcern) }); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (db && db.writeConcern) { | ||
|  |     return Object.assign(target, { writeConcern: Object.assign({}, db.writeConcern) }); | ||
|  |   } | ||
|  | 
 | ||
|  |   return target; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Checks if a given value is a Promise | ||
|  |  * | ||
|  |  * @param {*} maybePromise | ||
|  |  * @return true if the provided value is a Promise | ||
|  |  */ | ||
|  | function isPromiseLike(maybePromise) { | ||
|  |   return maybePromise && typeof maybePromise.then === 'function'; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Applies collation to a given command. | ||
|  |  * | ||
|  |  * @param {object} [command] the command on which to apply collation | ||
|  |  * @param {(Cursor|Collection)} [target] target of command | ||
|  |  * @param {object} [options] options containing collation settings | ||
|  |  */ | ||
|  | function decorateWithCollation(command, target, options) { | ||
|  |   const topology = (target.s && target.s.topology) || target.topology; | ||
|  | 
 | ||
|  |   if (!topology) { | ||
|  |     throw new TypeError('parameter "target" is missing a topology'); | ||
|  |   } | ||
|  | 
 | ||
|  |   const capabilities = topology.capabilities(); | ||
|  |   if (options.collation && typeof options.collation === 'object') { | ||
|  |     if (capabilities && capabilities.commandsTakeCollation) { | ||
|  |       command.collation = options.collation; | ||
|  |     } else { | ||
|  |       throw new MongoError(`Current topology does not support collation`); | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Applies a read concern to a given command. | ||
|  |  * | ||
|  |  * @param {object} command the command on which to apply the read concern | ||
|  |  * @param {Collection} coll the parent collection of the operation calling this method | ||
|  |  */ | ||
|  | function decorateWithReadConcern(command, coll, options) { | ||
|  |   if (options && options.session && options.session.inTransaction()) { | ||
|  |     return; | ||
|  |   } | ||
|  |   let readConcern = Object.assign({}, command.readConcern || {}); | ||
|  |   if (coll.s.readConcern) { | ||
|  |     Object.assign(readConcern, coll.s.readConcern); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (Object.keys(readConcern).length > 0) { | ||
|  |     Object.assign(command, { readConcern: readConcern }); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Applies an explain to a given command. | ||
|  |  * @internal | ||
|  |  * | ||
|  |  * @param {object} command - the command on which to apply the explain | ||
|  |  * @param {Explain} explain - the options containing the explain verbosity | ||
|  |  * @return the new command | ||
|  |  */ | ||
|  | function decorateWithExplain(command, explain) { | ||
|  |   if (command.explain) { | ||
|  |     return command; | ||
|  |   } | ||
|  | 
 | ||
|  |   return { explain: command, verbosity: explain.verbosity }; | ||
|  | } | ||
|  | 
 | ||
|  | const nodejsMajorVersion = +process.version.split('.')[0].substring(1); | ||
|  | const emitProcessWarning = msg => | ||
|  |   nodejsMajorVersion <= 6 | ||
|  |     ? process.emitWarning(msg, 'DeprecationWarning', MONGODB_WARNING_CODE) | ||
|  |     : process.emitWarning(msg, { type: 'DeprecationWarning', code: MONGODB_WARNING_CODE }); | ||
|  | // eslint-disable-next-line no-console
 | ||
|  | const emitConsoleWarning = msg => console.error(msg); | ||
|  | const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Default message handler for generating deprecation warnings. | ||
|  |  * | ||
|  |  * @param {string} name function name | ||
|  |  * @param {string} option option name | ||
|  |  * @return {string} warning message | ||
|  |  * @ignore | ||
|  |  * @api private | ||
|  |  */ | ||
|  | function defaultMsgHandler(name, option) { | ||
|  |   return `${name} option [${option}] is deprecated and will be removed in a later version.`; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Deprecates a given function's options. | ||
|  |  * | ||
|  |  * @param {object} config configuration for deprecation | ||
|  |  * @param {string} config.name function name | ||
|  |  * @param {Array} config.deprecatedOptions options to deprecate | ||
|  |  * @param {number} config.optionsIndex index of options object in function arguments array | ||
|  |  * @param {function} [config.msgHandler] optional custom message handler to generate warnings | ||
|  |  * @param {function} fn the target function of deprecation | ||
|  |  * @return {function} modified function that warns once per deprecated option, and executes original function | ||
|  |  * @ignore | ||
|  |  * @api private | ||
|  |  */ | ||
|  | function deprecateOptions(config, fn) { | ||
|  |   if (process.noDeprecation === true) { | ||
|  |     return fn; | ||
|  |   } | ||
|  | 
 | ||
|  |   const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler; | ||
|  | 
 | ||
|  |   const optionsWarned = new Set(); | ||
|  |   function deprecated() { | ||
|  |     const options = arguments[config.optionsIndex]; | ||
|  | 
 | ||
|  |     // ensure options is a valid, non-empty object, otherwise short-circuit
 | ||
|  |     if (!isObject(options) || Object.keys(options).length === 0) { | ||
|  |       return fn.apply(this, arguments); | ||
|  |     } | ||
|  | 
 | ||
|  |     config.deprecatedOptions.forEach(deprecatedOption => { | ||
|  |       if ( | ||
|  |         Object.prototype.hasOwnProperty.call(options, deprecatedOption) && | ||
|  |         !optionsWarned.has(deprecatedOption) | ||
|  |       ) { | ||
|  |         optionsWarned.add(deprecatedOption); | ||
|  |         const msg = msgHandler(config.name, deprecatedOption); | ||
|  |         emitDeprecationWarning(msg); | ||
|  |         if (this && this.getLogger) { | ||
|  |           const logger = this.getLogger(); | ||
|  |           if (logger) { | ||
|  |             logger.warn(msg); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     return fn.apply(this, arguments); | ||
|  |   } | ||
|  | 
 | ||
|  |   // These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80
 | ||
|  |   // The wrapper will keep the same prototype as fn to maintain prototype chain
 | ||
|  |   Object.setPrototypeOf(deprecated, fn); | ||
|  |   if (fn.prototype) { | ||
|  |     // Setting this (rather than using Object.setPrototype, as above) ensures
 | ||
|  |     // that calling the unwrapped constructor gives an instanceof the wrapped
 | ||
|  |     // constructor.
 | ||
|  |     deprecated.prototype = fn.prototype; | ||
|  |   } | ||
|  | 
 | ||
|  |   return deprecated; | ||
|  | } | ||
|  | 
 | ||
|  | const SUPPORTS = {}; | ||
|  | // Test asyncIterator support
 | ||
|  | try { | ||
|  |   require('./async/async_iterator'); | ||
|  |   SUPPORTS.ASYNC_ITERATOR = true; | ||
|  | } catch (e) { | ||
|  |   SUPPORTS.ASYNC_ITERATOR = false; | ||
|  | } | ||
|  | 
 | ||
|  | class MongoDBNamespace { | ||
|  |   constructor(db, collection) { | ||
|  |     this.db = db; | ||
|  |     this.collection = collection; | ||
|  |   } | ||
|  | 
 | ||
|  |   toString() { | ||
|  |     return this.collection ? `${this.db}.${this.collection}` : this.db; | ||
|  |   } | ||
|  | 
 | ||
|  |   withCollection(collection) { | ||
|  |     return new MongoDBNamespace(this.db, collection); | ||
|  |   } | ||
|  | 
 | ||
|  |   static fromString(namespace) { | ||
|  |     if (!namespace) { | ||
|  |       throw new Error(`Cannot parse namespace from "${namespace}"`); | ||
|  |     } | ||
|  | 
 | ||
|  |     const index = namespace.indexOf('.'); | ||
|  |     return new MongoDBNamespace(namespace.substring(0, index), namespace.substring(index + 1)); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function* makeCounter(seed) { | ||
|  |   let count = seed || 0; | ||
|  |   while (true) { | ||
|  |     const newCount = count; | ||
|  |     count += 1; | ||
|  |     yield newCount; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Helper function for either accepting a callback, or returning a promise | ||
|  |  * | ||
|  |  * @param {Object} parent an instance of parent with promiseLibrary. | ||
|  |  * @param {object} parent.s an object containing promiseLibrary. | ||
|  |  * @param {function} parent.s.promiseLibrary an object containing promiseLibrary. | ||
|  |  * @param {[Function]} callback an optional callback. | ||
|  |  * @param {Function} fn A function that takes a callback | ||
|  |  * @returns {Promise|void} Returns nothing if a callback is supplied, else returns a Promise. | ||
|  |  */ | ||
|  | function maybePromise(parent, callback, fn) { | ||
|  |   const PromiseLibrary = (parent && parent.s && parent.s.promiseLibrary) || Promise; | ||
|  | 
 | ||
|  |   let result; | ||
|  |   if (typeof callback !== 'function') { | ||
|  |     result = new PromiseLibrary((resolve, reject) => { | ||
|  |       callback = (err, res) => { | ||
|  |         if (err) return reject(err); | ||
|  |         resolve(res); | ||
|  |       }; | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   fn(function(err, res) { | ||
|  |     if (err != null) { | ||
|  |       try { | ||
|  |         callback(err); | ||
|  |       } catch (error) { | ||
|  |         return process.nextTick(() => { | ||
|  |           throw error; | ||
|  |         }); | ||
|  |       } | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     callback(err, res); | ||
|  |   }); | ||
|  | 
 | ||
|  |   return result; | ||
|  | } | ||
|  | 
 | ||
|  | function now() { | ||
|  |   const hrtime = process.hrtime(); | ||
|  |   return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000); | ||
|  | } | ||
|  | 
 | ||
|  | function calculateDurationInMs(started) { | ||
|  |   if (typeof started !== 'number') { | ||
|  |     throw TypeError('numeric value required to calculate duration'); | ||
|  |   } | ||
|  | 
 | ||
|  |   const elapsed = now() - started; | ||
|  |   return elapsed < 0 ? 0 : elapsed; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates an interval timer which is able to be woken up sooner than | ||
|  |  * the interval. The timer will also debounce multiple calls to wake | ||
|  |  * ensuring that the function is only ever called once within a minimum | ||
|  |  * interval window. | ||
|  |  * | ||
|  |  * @param {function} fn An async function to run on an interval, must accept a `callback` as its only parameter | ||
|  |  * @param {object} [options] Optional settings | ||
|  |  * @param {number} [options.interval] The interval at which to run the provided function | ||
|  |  * @param {number} [options.minInterval] The minimum time which must pass between invocations of the provided function | ||
|  |  * @param {boolean} [options.immediate] Execute the function immediately when the interval is started | ||
|  |  */ | ||
|  | function makeInterruptableAsyncInterval(fn, options) { | ||
|  |   let timerId; | ||
|  |   let lastCallTime; | ||
|  |   let lastWakeTime; | ||
|  |   let stopped = false; | ||
|  | 
 | ||
|  |   options = options || {}; | ||
|  |   const interval = options.interval || 1000; | ||
|  |   const minInterval = options.minInterval || 500; | ||
|  |   const immediate = typeof options.immediate === 'boolean' ? options.immediate : false; | ||
|  |   const clock = typeof options.clock === 'function' ? options.clock : now; | ||
|  | 
 | ||
|  |   function wake() { | ||
|  |     const currentTime = clock(); | ||
|  |     const timeSinceLastWake = currentTime - lastWakeTime; | ||
|  |     const timeSinceLastCall = currentTime - lastCallTime; | ||
|  |     const timeUntilNextCall = interval - timeSinceLastCall; | ||
|  |     lastWakeTime = currentTime; | ||
|  | 
 | ||
|  |     // For the streaming protocol: there is nothing obviously stopping this
 | ||
|  |     // interval from being woken up again while we are waiting "infinitely"
 | ||
|  |     // for `fn` to be called again`. Since the function effectively
 | ||
|  |     // never completes, the `timeUntilNextCall` will continue to grow
 | ||
|  |     // negatively unbounded, so it will never trigger a reschedule here.
 | ||
|  | 
 | ||
|  |     // debounce multiple calls to wake within the `minInterval`
 | ||
|  |     if (timeSinceLastWake < minInterval) { | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     // reschedule a call as soon as possible, ensuring the call never happens
 | ||
|  |     // faster than the `minInterval`
 | ||
|  |     if (timeUntilNextCall > minInterval) { | ||
|  |       reschedule(minInterval); | ||
|  |     } | ||
|  | 
 | ||
|  |     // This is possible in virtualized environments like AWS Lambda where our
 | ||
|  |     // clock is unreliable. In these cases the timer is "running" but never
 | ||
|  |     // actually completes, so we want to execute immediately and then attempt
 | ||
|  |     // to reschedule.
 | ||
|  |     if (timeUntilNextCall < 0) { | ||
|  |       executeAndReschedule(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function stop() { | ||
|  |     stopped = true; | ||
|  |     if (timerId) { | ||
|  |       clearTimeout(timerId); | ||
|  |       timerId = null; | ||
|  |     } | ||
|  | 
 | ||
|  |     lastCallTime = 0; | ||
|  |     lastWakeTime = 0; | ||
|  |   } | ||
|  | 
 | ||
|  |   function reschedule(ms) { | ||
|  |     if (stopped) return; | ||
|  |     clearTimeout(timerId); | ||
|  |     timerId = setTimeout(executeAndReschedule, ms || interval); | ||
|  |   } | ||
|  | 
 | ||
|  |   function executeAndReschedule() { | ||
|  |     lastWakeTime = 0; | ||
|  |     lastCallTime = clock(); | ||
|  | 
 | ||
|  |     fn(err => { | ||
|  |       if (err) throw err; | ||
|  |       reschedule(interval); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (immediate) { | ||
|  |     executeAndReschedule(); | ||
|  |   } else { | ||
|  |     lastCallTime = clock(); | ||
|  |     reschedule(); | ||
|  |   } | ||
|  | 
 | ||
|  |   return { wake, stop }; | ||
|  | } | ||
|  | 
 | ||
|  | function hasAtomicOperators(doc) { | ||
|  |   if (Array.isArray(doc)) { | ||
|  |     return doc.reduce((err, u) => err || hasAtomicOperators(u), null); | ||
|  |   } | ||
|  | 
 | ||
|  |   return ( | ||
|  |     Object.keys(typeof doc.toBSON !== 'function' ? doc : doc.toBSON()) | ||
|  |       .map(k => k[0]) | ||
|  |       .indexOf('$') >= 0 | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * When the driver used emitWarning the code will be equal to this. | ||
|  |  * @public | ||
|  |  * | ||
|  |  * @example | ||
|  |  * ```js
 | ||
|  |  * process.on('warning', (warning) => { | ||
|  |  *  if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)') | ||
|  |  * }) | ||
|  |  * ```
 | ||
|  |  */ | ||
|  | const MONGODB_WARNING_CODE = 'MONGODB DRIVER'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @internal | ||
|  |  * @param {string} message - message to warn about | ||
|  |  */ | ||
|  | function emitWarning(message) { | ||
|  |   if (process.emitWarning) { | ||
|  |     return nodejsMajorVersion <= 6 | ||
|  |       ? process.emitWarning(message, undefined, MONGODB_WARNING_CODE) | ||
|  |       : process.emitWarning(message, { code: MONGODB_WARNING_CODE }); | ||
|  |   } else { | ||
|  |     // Approximate the style of print out on node versions pre 8.x
 | ||
|  |     // eslint-disable-next-line no-console
 | ||
|  |     return console.error(`[${MONGODB_WARNING_CODE}] Warning:`, message); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const emittedWarnings = new Set(); | ||
|  | /** | ||
|  |  * Will emit a warning once for the duration of the application. | ||
|  |  * Uses the message to identify if it has already been emitted | ||
|  |  * so using string interpolation can cause multiple emits | ||
|  |  * @internal | ||
|  |  * @param {string} message - message to warn about | ||
|  |  */ | ||
|  | function emitWarningOnce(message) { | ||
|  |   if (!emittedWarnings.has(message)) { | ||
|  |     emittedWarnings.add(message); | ||
|  |     return emitWarning(message); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function isSuperset(set, subset) { | ||
|  |   set = Array.isArray(set) ? new Set(set) : set; | ||
|  |   subset = Array.isArray(subset) ? new Set(subset) : subset; | ||
|  |   for (const elem of subset) { | ||
|  |     if (!set.has(elem)) { | ||
|  |       return false; | ||
|  |     } | ||
|  |   } | ||
|  |   return true; | ||
|  | } | ||
|  | 
 | ||
|  | function isRecord(value, requiredKeys) { | ||
|  |   const toString = Object.prototype.toString; | ||
|  |   const hasOwnProperty = Object.prototype.hasOwnProperty; | ||
|  |   const isObject = v => toString.call(v) === '[object Object]'; | ||
|  |   if (!isObject(value)) { | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   const ctor = value.constructor; | ||
|  |   if (ctor && ctor.prototype) { | ||
|  |     if (!isObject(ctor.prototype)) { | ||
|  |       return false; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Check to see if some method exists from the Object exists
 | ||
|  |     if (!hasOwnProperty.call(ctor.prototype, 'isPrototypeOf')) { | ||
|  |       return false; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (requiredKeys) { | ||
|  |     const keys = Object.keys(value); | ||
|  |     return isSuperset(keys, requiredKeys); | ||
|  |   } | ||
|  | 
 | ||
|  |   return true; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Make a deep copy of an object | ||
|  |  * | ||
|  |  * NOTE: This is not meant to be the perfect implementation of a deep copy, | ||
|  |  * but instead something that is good enough for the purposes of | ||
|  |  * command monitoring. | ||
|  |  */ | ||
|  | function deepCopy(value) { | ||
|  |   if (value == null) { | ||
|  |     return value; | ||
|  |   } else if (Array.isArray(value)) { | ||
|  |     return value.map(item => deepCopy(item)); | ||
|  |   } else if (isRecord(value)) { | ||
|  |     const res = {}; | ||
|  |     for (const key in value) { | ||
|  |       res[key] = deepCopy(value[key]); | ||
|  |     } | ||
|  |     return res; | ||
|  |   } | ||
|  | 
 | ||
|  |   const ctor = value.constructor; | ||
|  |   if (ctor) { | ||
|  |     switch (ctor.name.toLowerCase()) { | ||
|  |       case 'date': | ||
|  |         return new ctor(Number(value)); | ||
|  |       case 'map': | ||
|  |         return new Map(value); | ||
|  |       case 'set': | ||
|  |         return new Set(value); | ||
|  |       case 'buffer': | ||
|  |         return Buffer.from(value); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return value; | ||
|  | } | ||
|  | /** | ||
|  |  * @param {{version: string}} pkg | ||
|  |  * @returns {{ major: number; minor: number; patch: number }} | ||
|  |  */ | ||
|  | function parsePackageVersion(pkg) { | ||
|  |   const versionParts = pkg.version.split('.').map(n => Number.parseInt(n, 10)); | ||
|  |   return { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] }; | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |   filterOptions, | ||
|  |   mergeOptions, | ||
|  |   translateOptions, | ||
|  |   shallowClone, | ||
|  |   getSingleProperty, | ||
|  |   checkCollectionName, | ||
|  |   toError, | ||
|  |   formattedOrderClause, | ||
|  |   parseIndexOptions, | ||
|  |   normalizeHintField, | ||
|  |   handleCallback, | ||
|  |   decorateCommand, | ||
|  |   isObject, | ||
|  |   debugOptions, | ||
|  |   MAX_JS_INT: Number.MAX_SAFE_INTEGER + 1, | ||
|  |   conditionallyMergeWriteConcern, | ||
|  |   executeLegacyOperation, | ||
|  |   applyRetryableWrites, | ||
|  |   applyWriteConcern, | ||
|  |   isPromiseLike, | ||
|  |   decorateWithCollation, | ||
|  |   decorateWithReadConcern, | ||
|  |   decorateWithExplain, | ||
|  |   deprecateOptions, | ||
|  |   SUPPORTS, | ||
|  |   MongoDBNamespace, | ||
|  |   emitDeprecationWarning, | ||
|  |   makeCounter, | ||
|  |   maybePromise, | ||
|  |   now, | ||
|  |   calculateDurationInMs, | ||
|  |   makeInterruptableAsyncInterval, | ||
|  |   hasAtomicOperators, | ||
|  |   MONGODB_WARNING_CODE, | ||
|  |   emitWarning, | ||
|  |   emitWarningOnce, | ||
|  |   deepCopy, | ||
|  |   parsePackageVersion | ||
|  | }; |