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.
		
		
		
		
		
			
		
			
				
					475 lines
				
				16 KiB
			
		
		
			
		
	
	
					475 lines
				
				16 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | const MONGODB_ERROR_CODES = require('../error_codes').MONGODB_ERROR_CODES; | ||
|  | const applyWriteConcern = require('../utils').applyWriteConcern; | ||
|  | const Code = require('../core').BSON.Code; | ||
|  | const debugOptions = require('../utils').debugOptions; | ||
|  | const handleCallback = require('../utils').handleCallback; | ||
|  | const MongoError = require('../core').MongoError; | ||
|  | const parseIndexOptions = require('../utils').parseIndexOptions; | ||
|  | const ReadPreference = require('../core').ReadPreference; | ||
|  | const toError = require('../utils').toError; | ||
|  | const extractCommand = require('../command_utils').extractCommand; | ||
|  | const CONSTANTS = require('../constants'); | ||
|  | const MongoDBNamespace = require('../utils').MongoDBNamespace; | ||
|  | 
 | ||
|  | const debugFields = [ | ||
|  |   'authSource', | ||
|  |   'w', | ||
|  |   'wtimeout', | ||
|  |   'j', | ||
|  |   'native_parser', | ||
|  |   'forceServerObjectId', | ||
|  |   'serializeFunctions', | ||
|  |   'raw', | ||
|  |   'promoteLongs', | ||
|  |   'promoteValues', | ||
|  |   'promoteBuffers', | ||
|  |   'bsonRegExp', | ||
|  |   'bufferMaxEntries', | ||
|  |   'numberOfRetries', | ||
|  |   'retryMiliSeconds', | ||
|  |   'readPreference', | ||
|  |   'pkFactory', | ||
|  |   'parentDb', | ||
|  |   'promiseLibrary', | ||
|  |   'noListener' | ||
|  | ]; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates an index on the db and collection. | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to create an index. | ||
|  |  * @param {string} name Name of the collection to create the index on. | ||
|  |  * @param {(string|object)} fieldOrSpec Defines the index. | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.createIndex for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback | ||
|  |  */ | ||
|  | function createIndex(db, name, fieldOrSpec, options, callback) { | ||
|  |   // Get the write concern options
 | ||
|  |   let finalOptions = Object.assign({}, { readPreference: ReadPreference.PRIMARY }, options); | ||
|  |   finalOptions = applyWriteConcern(finalOptions, { db }, options); | ||
|  | 
 | ||
|  |   // Ensure we have a callback
 | ||
|  |   if (finalOptions.writeConcern && typeof callback !== 'function') { | ||
|  |     throw MongoError.create({ | ||
|  |       message: 'Cannot use a writeConcern without a provided callback', | ||
|  |       driver: true | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Did the user destroy the topology
 | ||
|  |   if (db.serverConfig && db.serverConfig.isDestroyed()) | ||
|  |     return callback(new MongoError('topology was destroyed')); | ||
|  | 
 | ||
|  |   // Attempt to run using createIndexes command
 | ||
|  |   createIndexUsingCreateIndexes(db, name, fieldOrSpec, finalOptions, (err, result) => { | ||
|  |     if (err == null) return handleCallback(callback, err, result); | ||
|  | 
 | ||
|  |     /** | ||
|  |      * The following errors mean that the server recognized `createIndex` as a command so we don't need to fallback to an insert: | ||
|  |      * 67 = 'CannotCreateIndex' (malformed index options) | ||
|  |      * 85 = 'IndexOptionsConflict' (index already exists with different options) | ||
|  |      * 86 = 'IndexKeySpecsConflict' (index already exists with the same name) | ||
|  |      * 11000 = 'DuplicateKey' (couldn't build unique index because of dupes) | ||
|  |      * 11600 = 'InterruptedAtShutdown' (interrupted at shutdown) | ||
|  |      * 197 = 'InvalidIndexSpecificationOption' (`_id` with `background: true`) | ||
|  |      */ | ||
|  |     if ( | ||
|  |       err.code === MONGODB_ERROR_CODES.CannotCreateIndex || | ||
|  |       err.code === MONGODB_ERROR_CODES.DuplicateKey || | ||
|  |       err.code === MONGODB_ERROR_CODES.IndexOptionsConflict || | ||
|  |       err.code === MONGODB_ERROR_CODES.IndexKeySpecsConflict || | ||
|  |       err.code === MONGODB_ERROR_CODES.InterruptedAtShutdown || | ||
|  |       err.code === MONGODB_ERROR_CODES.InvalidIndexSpecificationOption | ||
|  |     ) { | ||
|  |       return handleCallback(callback, err, result); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Create command
 | ||
|  |     const doc = createCreateIndexCommand(db, name, fieldOrSpec, options); | ||
|  |     // Set no key checking
 | ||
|  |     finalOptions.checkKeys = false; | ||
|  |     // Insert document
 | ||
|  |     db.s.topology.insert( | ||
|  |       db.s.namespace.withCollection(CONSTANTS.SYSTEM_INDEX_COLLECTION), | ||
|  |       doc, | ||
|  |       finalOptions, | ||
|  |       (err, result) => { | ||
|  |         if (callback == null) return; | ||
|  |         if (err) return handleCallback(callback, err); | ||
|  |         if (result == null) return handleCallback(callback, null, null); | ||
|  |         if (result.result.writeErrors) | ||
|  |           return handleCallback(callback, MongoError.create(result.result.writeErrors[0]), null); | ||
|  |         handleCallback(callback, null, doc.name); | ||
|  |       } | ||
|  |     ); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | // Add listeners to topology
 | ||
|  | function createListener(db, e, object) { | ||
|  |   function listener(err) { | ||
|  |     if (object.listeners(e).length > 0) { | ||
|  |       object.emit(e, err, db); | ||
|  | 
 | ||
|  |       // Emit on all associated db's if available
 | ||
|  |       for (let i = 0; i < db.s.children.length; i++) { | ||
|  |         db.s.children[i].emit(e, err, db.s.children[i]); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |   return listener; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Ensures that an index exists. If it does not, creates it. | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to ensure the index. | ||
|  |  * @param {string} name The index name | ||
|  |  * @param {(string|object)} fieldOrSpec Defines the index. | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.ensureIndex for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback | ||
|  |  */ | ||
|  | function ensureIndex(db, name, fieldOrSpec, options, callback) { | ||
|  |   // Get the write concern options
 | ||
|  |   const finalOptions = applyWriteConcern({}, { db }, options); | ||
|  |   // Create command
 | ||
|  |   const selector = createCreateIndexCommand(db, name, fieldOrSpec, options); | ||
|  |   const index_name = selector.name; | ||
|  | 
 | ||
|  |   // Did the user destroy the topology
 | ||
|  |   if (db.serverConfig && db.serverConfig.isDestroyed()) | ||
|  |     return callback(new MongoError('topology was destroyed')); | ||
|  | 
 | ||
|  |   // Merge primary readPreference
 | ||
|  |   finalOptions.readPreference = ReadPreference.PRIMARY; | ||
|  | 
 | ||
|  |   // Check if the index already exists
 | ||
|  |   indexInformation(db, name, finalOptions, (err, indexInformation) => { | ||
|  |     if (err != null && err.code !== MONGODB_ERROR_CODES.NamespaceNotFound) { | ||
|  |       return handleCallback(callback, err, null); | ||
|  |     } | ||
|  |     // If the index does not exist, create it
 | ||
|  |     if (indexInformation == null || !indexInformation[index_name]) { | ||
|  |       createIndex(db, name, fieldOrSpec, options, callback); | ||
|  |     } else { | ||
|  |       if (typeof callback === 'function') return handleCallback(callback, null, index_name); | ||
|  |     } | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Evaluate JavaScript on the server | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance. | ||
|  |  * @param {Code} code JavaScript to execute on server. | ||
|  |  * @param {(object|array)} parameters The parameters for the call. | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.eval for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The results callback | ||
|  |  * @deprecated Eval is deprecated on MongoDB 3.2 and forward | ||
|  |  */ | ||
|  | function evaluate(db, code, parameters, options, callback) { | ||
|  |   let finalCode = code; | ||
|  |   let finalParameters = []; | ||
|  | 
 | ||
|  |   // Did the user destroy the topology
 | ||
|  |   if (db.serverConfig && db.serverConfig.isDestroyed()) | ||
|  |     return callback(new MongoError('topology was destroyed')); | ||
|  | 
 | ||
|  |   // If not a code object translate to one
 | ||
|  |   if (!(finalCode && finalCode._bsontype === 'Code')) finalCode = new Code(finalCode); | ||
|  |   // Ensure the parameters are correct
 | ||
|  |   if (parameters != null && !Array.isArray(parameters) && typeof parameters !== 'function') { | ||
|  |     finalParameters = [parameters]; | ||
|  |   } else if (parameters != null && Array.isArray(parameters) && typeof parameters !== 'function') { | ||
|  |     finalParameters = parameters; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Create execution selector
 | ||
|  |   let cmd = { $eval: finalCode, args: finalParameters }; | ||
|  |   // Check if the nolock parameter is passed in
 | ||
|  |   if (options['nolock']) { | ||
|  |     cmd['nolock'] = options['nolock']; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Set primary read preference
 | ||
|  |   options.readPreference = new ReadPreference(ReadPreference.PRIMARY); | ||
|  | 
 | ||
|  |   // Execute the command
 | ||
|  |   executeCommand(db, cmd, options, (err, result) => { | ||
|  |     if (err) return handleCallback(callback, err, null); | ||
|  |     if (result && result.ok === 1) return handleCallback(callback, null, result.retval); | ||
|  |     if (result) | ||
|  |       return handleCallback( | ||
|  |         callback, | ||
|  |         MongoError.create({ message: `eval failed: ${result.errmsg}`, driver: true }), | ||
|  |         null | ||
|  |       ); | ||
|  |     handleCallback(callback, err, result); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Execute a command | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to execute the command. | ||
|  |  * @param {object} command The command hash | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.command for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback | ||
|  |  */ | ||
|  | function executeCommand(db, command, options, callback) { | ||
|  |   // Did the user destroy the topology
 | ||
|  |   if (db.serverConfig && db.serverConfig.isDestroyed()) | ||
|  |     return callback(new MongoError('topology was destroyed')); | ||
|  |   // Get the db name we are executing against
 | ||
|  |   const dbName = options.dbName || options.authdb || db.databaseName; | ||
|  | 
 | ||
|  |   // Convert the readPreference if its not a write
 | ||
|  |   options.readPreference = ReadPreference.resolve(db, options); | ||
|  | 
 | ||
|  |   // Debug information
 | ||
|  |   if (db.s.logger.isDebug()) { | ||
|  |     const extractedCommand = extractCommand(command); | ||
|  |     db.s.logger.debug( | ||
|  |       `executing command ${JSON.stringify( | ||
|  |         extractedCommand.shouldRedact ? `${extractedCommand.name} details REDACTED` : command | ||
|  |       )} against ${dbName}.$cmd with options [${JSON.stringify( | ||
|  |         debugOptions(debugFields, options) | ||
|  |       )}]`
 | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Execute command
 | ||
|  |   db.s.topology.command(db.s.namespace.withCollection('$cmd'), command, options, (err, result) => { | ||
|  |     if (err) return handleCallback(callback, err); | ||
|  |     if (options.full) return handleCallback(callback, null, result); | ||
|  |     handleCallback(callback, null, result.result); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Runs a command on the database as admin. | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to execute the command. | ||
|  |  * @param {object} command The command hash | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.executeDbAdminCommand for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback | ||
|  |  */ | ||
|  | function executeDbAdminCommand(db, command, options, callback) { | ||
|  |   const namespace = new MongoDBNamespace('admin', '$cmd'); | ||
|  | 
 | ||
|  |   db.s.topology.command(namespace, command, options, (err, result) => { | ||
|  |     // Did the user destroy the topology
 | ||
|  |     if (db.serverConfig && db.serverConfig.isDestroyed()) { | ||
|  |       return callback(new MongoError('topology was destroyed')); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (err) return handleCallback(callback, err); | ||
|  |     handleCallback(callback, null, result.result); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Retrieves this collections index info. | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to retrieve the index info. | ||
|  |  * @param {string} name The name of the collection. | ||
|  |  * @param {object} [options] Optional settings. See Db.prototype.indexInformation for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback | ||
|  |  */ | ||
|  | function indexInformation(db, name, options, callback) { | ||
|  |   // If we specified full information
 | ||
|  |   const full = options['full'] == null ? false : options['full']; | ||
|  | 
 | ||
|  |   // Did the user destroy the topology
 | ||
|  |   if (db.serverConfig && db.serverConfig.isDestroyed()) | ||
|  |     return callback(new MongoError('topology was destroyed')); | ||
|  |   // Process all the results from the index command and collection
 | ||
|  |   function processResults(indexes) { | ||
|  |     // Contains all the information
 | ||
|  |     let info = {}; | ||
|  |     // Process all the indexes
 | ||
|  |     for (let i = 0; i < indexes.length; i++) { | ||
|  |       const index = indexes[i]; | ||
|  |       // Let's unpack the object
 | ||
|  |       info[index.name] = []; | ||
|  |       for (let name in index.key) { | ||
|  |         info[index.name].push([name, index.key[name]]); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     return info; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Get the list of indexes of the specified collection
 | ||
|  |   db.collection(name) | ||
|  |     .listIndexes(options) | ||
|  |     .toArray((err, indexes) => { | ||
|  |       if (err) return callback(toError(err)); | ||
|  |       if (!Array.isArray(indexes)) return handleCallback(callback, null, []); | ||
|  |       if (full) return handleCallback(callback, null, indexes); | ||
|  |       handleCallback(callback, null, processResults(indexes)); | ||
|  |     }); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Retrieve the current profiling information for MongoDB | ||
|  |  * | ||
|  |  * @method | ||
|  |  * @param {Db} db The Db instance on which to retrieve the profiling info. | ||
|  |  * @param {Object} [options] Optional settings. See Db.protoype.profilingInfo for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback. | ||
|  |  * @deprecated Query the system.profile collection directly. | ||
|  |  */ | ||
|  | function profilingInfo(db, options, callback) { | ||
|  |   try { | ||
|  |     db.collection('system.profile') | ||
|  |       .find({}, options) | ||
|  |       .toArray(callback); | ||
|  |   } catch (err) { | ||
|  |     return callback(err, null); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | // Validate the database name
 | ||
|  | function validateDatabaseName(databaseName) { | ||
|  |   if (typeof databaseName !== 'string') | ||
|  |     throw MongoError.create({ message: 'database name must be a string', driver: true }); | ||
|  |   if (databaseName.length === 0) | ||
|  |     throw MongoError.create({ message: 'database name cannot be the empty string', driver: true }); | ||
|  |   if (databaseName === '$external') return; | ||
|  | 
 | ||
|  |   const invalidChars = [' ', '.', '$', '/', '\\']; | ||
|  |   for (let i = 0; i < invalidChars.length; i++) { | ||
|  |     if (databaseName.indexOf(invalidChars[i]) !== -1) | ||
|  |       throw MongoError.create({ | ||
|  |         message: "database names cannot contain the character '" + invalidChars[i] + "'", | ||
|  |         driver: true | ||
|  |       }); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create the command object for Db.prototype.createIndex. | ||
|  |  * | ||
|  |  * @param {Db} db The Db instance on which to create the command. | ||
|  |  * @param {string} name Name of the collection to create the index on. | ||
|  |  * @param {(string|object)} fieldOrSpec Defines the index. | ||
|  |  * @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options. | ||
|  |  * @return {Object} The insert command object. | ||
|  |  */ | ||
|  | function createCreateIndexCommand(db, name, fieldOrSpec, options) { | ||
|  |   const indexParameters = parseIndexOptions(fieldOrSpec); | ||
|  |   const fieldHash = indexParameters.fieldHash; | ||
|  | 
 | ||
|  |   // Generate the index name
 | ||
|  |   const indexName = typeof options.name === 'string' ? options.name : indexParameters.name; | ||
|  |   const selector = { | ||
|  |     ns: db.s.namespace.withCollection(name).toString(), | ||
|  |     key: fieldHash, | ||
|  |     name: indexName | ||
|  |   }; | ||
|  | 
 | ||
|  |   // Ensure we have a correct finalUnique
 | ||
|  |   const finalUnique = options == null || 'object' === typeof options ? false : options; | ||
|  |   // Set up options
 | ||
|  |   options = options == null || typeof options === 'boolean' ? {} : options; | ||
|  | 
 | ||
|  |   // Add all the options
 | ||
|  |   const keysToOmit = Object.keys(selector); | ||
|  |   for (let optionName in options) { | ||
|  |     if (keysToOmit.indexOf(optionName) === -1) { | ||
|  |       selector[optionName] = options[optionName]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (selector['unique'] == null) selector['unique'] = finalUnique; | ||
|  | 
 | ||
|  |   // Remove any write concern operations
 | ||
|  |   const removeKeys = ['w', 'wtimeout', 'j', 'fsync', 'readPreference', 'session']; | ||
|  |   for (let i = 0; i < removeKeys.length; i++) { | ||
|  |     delete selector[removeKeys[i]]; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Return the command creation selector
 | ||
|  |   return selector; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Create index using the createIndexes command. | ||
|  |  * | ||
|  |  * @param {Db} db The Db instance on which to execute the command. | ||
|  |  * @param {string} name Name of the collection to create the index on. | ||
|  |  * @param {(string|object)} fieldOrSpec Defines the index. | ||
|  |  * @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options. | ||
|  |  * @param {Db~resultCallback} [callback] The command result callback. | ||
|  |  */ | ||
|  | function createIndexUsingCreateIndexes(db, name, fieldOrSpec, options, callback) { | ||
|  |   // Build the index
 | ||
|  |   const indexParameters = parseIndexOptions(fieldOrSpec); | ||
|  |   // Generate the index name
 | ||
|  |   const indexName = typeof options.name === 'string' ? options.name : indexParameters.name; | ||
|  |   // Set up the index
 | ||
|  |   const indexes = [{ name: indexName, key: indexParameters.fieldHash }]; | ||
|  |   // merge all the options
 | ||
|  |   const keysToOmit = Object.keys(indexes[0]).concat([ | ||
|  |     'writeConcern', | ||
|  |     'w', | ||
|  |     'wtimeout', | ||
|  |     'j', | ||
|  |     'fsync', | ||
|  |     'readPreference', | ||
|  |     'session' | ||
|  |   ]); | ||
|  | 
 | ||
|  |   for (let optionName in options) { | ||
|  |     if (keysToOmit.indexOf(optionName) === -1) { | ||
|  |       indexes[0][optionName] = options[optionName]; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Get capabilities
 | ||
|  |   const capabilities = db.s.topology.capabilities(); | ||
|  | 
 | ||
|  |   // Did the user pass in a collation, check if our write server supports it
 | ||
|  |   if (indexes[0].collation && capabilities && !capabilities.commandsTakeCollation) { | ||
|  |     // Create a new error
 | ||
|  |     const error = new MongoError('server/primary/mongos does not support collation'); | ||
|  |     error.code = 67; | ||
|  |     // Return the error
 | ||
|  |     return callback(error); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Create command, apply write concern to command
 | ||
|  |   const cmd = applyWriteConcern({ createIndexes: name, indexes }, { db }, options); | ||
|  | 
 | ||
|  |   // ReadPreference primary
 | ||
|  |   options.readPreference = ReadPreference.PRIMARY; | ||
|  | 
 | ||
|  |   // Build the command
 | ||
|  |   executeCommand(db, cmd, options, (err, result) => { | ||
|  |     if (err) return handleCallback(callback, err, null); | ||
|  |     if (result.ok === 0) return handleCallback(callback, toError(result), null); | ||
|  |     // Return the indexName for backward compatibility
 | ||
|  |     handleCallback(callback, null, indexName); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |   createListener, | ||
|  |   createIndex, | ||
|  |   ensureIndex, | ||
|  |   evaluate, | ||
|  |   executeCommand, | ||
|  |   executeDbAdminCommand, | ||
|  |   indexInformation, | ||
|  |   profilingInfo, | ||
|  |   validateDatabaseName | ||
|  | }; |