db_ops.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. 'use strict';
  2. const MONGODB_ERROR_CODES = require('../error_codes').MONGODB_ERROR_CODES;
  3. const applyWriteConcern = require('../utils').applyWriteConcern;
  4. const Code = require('../core').BSON.Code;
  5. const debugOptions = require('../utils').debugOptions;
  6. const handleCallback = require('../utils').handleCallback;
  7. const MongoError = require('../core').MongoError;
  8. const parseIndexOptions = require('../utils').parseIndexOptions;
  9. const ReadPreference = require('../core').ReadPreference;
  10. const toError = require('../utils').toError;
  11. const extractCommand = require('../command_utils').extractCommand;
  12. const CONSTANTS = require('../constants');
  13. const MongoDBNamespace = require('../utils').MongoDBNamespace;
  14. const debugFields = [
  15. 'authSource',
  16. 'w',
  17. 'wtimeout',
  18. 'j',
  19. 'native_parser',
  20. 'forceServerObjectId',
  21. 'serializeFunctions',
  22. 'raw',
  23. 'promoteLongs',
  24. 'promoteValues',
  25. 'promoteBuffers',
  26. 'bsonRegExp',
  27. 'bufferMaxEntries',
  28. 'numberOfRetries',
  29. 'retryMiliSeconds',
  30. 'readPreference',
  31. 'pkFactory',
  32. 'parentDb',
  33. 'promiseLibrary',
  34. 'noListener'
  35. ];
  36. /**
  37. * Creates an index on the db and collection.
  38. * @method
  39. * @param {Db} db The Db instance on which to create an index.
  40. * @param {string} name Name of the collection to create the index on.
  41. * @param {(string|object)} fieldOrSpec Defines the index.
  42. * @param {object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
  43. * @param {Db~resultCallback} [callback] The command result callback
  44. */
  45. function createIndex(db, name, fieldOrSpec, options, callback) {
  46. // Get the write concern options
  47. let finalOptions = Object.assign({}, { readPreference: ReadPreference.PRIMARY }, options);
  48. finalOptions = applyWriteConcern(finalOptions, { db }, options);
  49. // Ensure we have a callback
  50. if (finalOptions.writeConcern && typeof callback !== 'function') {
  51. throw MongoError.create({
  52. message: 'Cannot use a writeConcern without a provided callback',
  53. driver: true
  54. });
  55. }
  56. // Did the user destroy the topology
  57. if (db.serverConfig && db.serverConfig.isDestroyed())
  58. return callback(new MongoError('topology was destroyed'));
  59. // Attempt to run using createIndexes command
  60. createIndexUsingCreateIndexes(db, name, fieldOrSpec, finalOptions, (err, result) => {
  61. if (err == null) return handleCallback(callback, err, result);
  62. /**
  63. * The following errors mean that the server recognized `createIndex` as a command so we don't need to fallback to an insert:
  64. * 67 = 'CannotCreateIndex' (malformed index options)
  65. * 85 = 'IndexOptionsConflict' (index already exists with different options)
  66. * 86 = 'IndexKeySpecsConflict' (index already exists with the same name)
  67. * 11000 = 'DuplicateKey' (couldn't build unique index because of dupes)
  68. * 11600 = 'InterruptedAtShutdown' (interrupted at shutdown)
  69. * 197 = 'InvalidIndexSpecificationOption' (`_id` with `background: true`)
  70. */
  71. if (
  72. err.code === MONGODB_ERROR_CODES.CannotCreateIndex ||
  73. err.code === MONGODB_ERROR_CODES.DuplicateKey ||
  74. err.code === MONGODB_ERROR_CODES.IndexOptionsConflict ||
  75. err.code === MONGODB_ERROR_CODES.IndexKeySpecsConflict ||
  76. err.code === MONGODB_ERROR_CODES.InterruptedAtShutdown ||
  77. err.code === MONGODB_ERROR_CODES.InvalidIndexSpecificationOption
  78. ) {
  79. return handleCallback(callback, err, result);
  80. }
  81. // Create command
  82. const doc = createCreateIndexCommand(db, name, fieldOrSpec, options);
  83. // Set no key checking
  84. finalOptions.checkKeys = false;
  85. // Insert document
  86. db.s.topology.insert(
  87. db.s.namespace.withCollection(CONSTANTS.SYSTEM_INDEX_COLLECTION),
  88. doc,
  89. finalOptions,
  90. (err, result) => {
  91. if (callback == null) return;
  92. if (err) return handleCallback(callback, err);
  93. if (result == null) return handleCallback(callback, null, null);
  94. if (result.result.writeErrors)
  95. return handleCallback(callback, MongoError.create(result.result.writeErrors[0]), null);
  96. handleCallback(callback, null, doc.name);
  97. }
  98. );
  99. });
  100. }
  101. // Add listeners to topology
  102. function createListener(db, e, object) {
  103. function listener(err) {
  104. if (object.listeners(e).length > 0) {
  105. object.emit(e, err, db);
  106. // Emit on all associated db's if available
  107. for (let i = 0; i < db.s.children.length; i++) {
  108. db.s.children[i].emit(e, err, db.s.children[i]);
  109. }
  110. }
  111. }
  112. return listener;
  113. }
  114. /**
  115. * Ensures that an index exists. If it does not, creates it.
  116. *
  117. * @method
  118. * @param {Db} db The Db instance on which to ensure the index.
  119. * @param {string} name The index name
  120. * @param {(string|object)} fieldOrSpec Defines the index.
  121. * @param {object} [options] Optional settings. See Db.prototype.ensureIndex for a list of options.
  122. * @param {Db~resultCallback} [callback] The command result callback
  123. */
  124. function ensureIndex(db, name, fieldOrSpec, options, callback) {
  125. // Get the write concern options
  126. const finalOptions = applyWriteConcern({}, { db }, options);
  127. // Create command
  128. const selector = createCreateIndexCommand(db, name, fieldOrSpec, options);
  129. const index_name = selector.name;
  130. // Did the user destroy the topology
  131. if (db.serverConfig && db.serverConfig.isDestroyed())
  132. return callback(new MongoError('topology was destroyed'));
  133. // Merge primary readPreference
  134. finalOptions.readPreference = ReadPreference.PRIMARY;
  135. // Check if the index already exists
  136. indexInformation(db, name, finalOptions, (err, indexInformation) => {
  137. if (err != null && err.code !== MONGODB_ERROR_CODES.NamespaceNotFound) {
  138. return handleCallback(callback, err, null);
  139. }
  140. // If the index does not exist, create it
  141. if (indexInformation == null || !indexInformation[index_name]) {
  142. createIndex(db, name, fieldOrSpec, options, callback);
  143. } else {
  144. if (typeof callback === 'function') return handleCallback(callback, null, index_name);
  145. }
  146. });
  147. }
  148. /**
  149. * Evaluate JavaScript on the server
  150. *
  151. * @method
  152. * @param {Db} db The Db instance.
  153. * @param {Code} code JavaScript to execute on server.
  154. * @param {(object|array)} parameters The parameters for the call.
  155. * @param {object} [options] Optional settings. See Db.prototype.eval for a list of options.
  156. * @param {Db~resultCallback} [callback] The results callback
  157. * @deprecated Eval is deprecated on MongoDB 3.2 and forward
  158. */
  159. function evaluate(db, code, parameters, options, callback) {
  160. let finalCode = code;
  161. let finalParameters = [];
  162. // Did the user destroy the topology
  163. if (db.serverConfig && db.serverConfig.isDestroyed())
  164. return callback(new MongoError('topology was destroyed'));
  165. // If not a code object translate to one
  166. if (!(finalCode && finalCode._bsontype === 'Code')) finalCode = new Code(finalCode);
  167. // Ensure the parameters are correct
  168. if (parameters != null && !Array.isArray(parameters) && typeof parameters !== 'function') {
  169. finalParameters = [parameters];
  170. } else if (parameters != null && Array.isArray(parameters) && typeof parameters !== 'function') {
  171. finalParameters = parameters;
  172. }
  173. // Create execution selector
  174. let cmd = { $eval: finalCode, args: finalParameters };
  175. // Check if the nolock parameter is passed in
  176. if (options['nolock']) {
  177. cmd['nolock'] = options['nolock'];
  178. }
  179. // Set primary read preference
  180. options.readPreference = new ReadPreference(ReadPreference.PRIMARY);
  181. // Execute the command
  182. executeCommand(db, cmd, options, (err, result) => {
  183. if (err) return handleCallback(callback, err, null);
  184. if (result && result.ok === 1) return handleCallback(callback, null, result.retval);
  185. if (result)
  186. return handleCallback(
  187. callback,
  188. MongoError.create({ message: `eval failed: ${result.errmsg}`, driver: true }),
  189. null
  190. );
  191. handleCallback(callback, err, result);
  192. });
  193. }
  194. /**
  195. * Execute a command
  196. *
  197. * @method
  198. * @param {Db} db The Db instance on which to execute the command.
  199. * @param {object} command The command hash
  200. * @param {object} [options] Optional settings. See Db.prototype.command for a list of options.
  201. * @param {Db~resultCallback} [callback] The command result callback
  202. */
  203. function executeCommand(db, command, options, callback) {
  204. // Did the user destroy the topology
  205. if (db.serverConfig && db.serverConfig.isDestroyed())
  206. return callback(new MongoError('topology was destroyed'));
  207. // Get the db name we are executing against
  208. const dbName = options.dbName || options.authdb || db.databaseName;
  209. // Convert the readPreference if its not a write
  210. options.readPreference = ReadPreference.resolve(db, options);
  211. // Debug information
  212. if (db.s.logger.isDebug()) {
  213. const extractedCommand = extractCommand(command);
  214. db.s.logger.debug(
  215. `executing command ${JSON.stringify(
  216. extractedCommand.shouldRedact ? `${extractedCommand.name} details REDACTED` : command
  217. )} against ${dbName}.$cmd with options [${JSON.stringify(
  218. debugOptions(debugFields, options)
  219. )}]`
  220. );
  221. }
  222. // Execute command
  223. db.s.topology.command(db.s.namespace.withCollection('$cmd'), command, options, (err, result) => {
  224. if (err) return handleCallback(callback, err);
  225. if (options.full) return handleCallback(callback, null, result);
  226. handleCallback(callback, null, result.result);
  227. });
  228. }
  229. /**
  230. * Runs a command on the database as admin.
  231. *
  232. * @method
  233. * @param {Db} db The Db instance on which to execute the command.
  234. * @param {object} command The command hash
  235. * @param {object} [options] Optional settings. See Db.prototype.executeDbAdminCommand for a list of options.
  236. * @param {Db~resultCallback} [callback] The command result callback
  237. */
  238. function executeDbAdminCommand(db, command, options, callback) {
  239. const namespace = new MongoDBNamespace('admin', '$cmd');
  240. db.s.topology.command(namespace, command, options, (err, result) => {
  241. // Did the user destroy the topology
  242. if (db.serverConfig && db.serverConfig.isDestroyed()) {
  243. return callback(new MongoError('topology was destroyed'));
  244. }
  245. if (err) return handleCallback(callback, err);
  246. handleCallback(callback, null, result.result);
  247. });
  248. }
  249. /**
  250. * Retrieves this collections index info.
  251. *
  252. * @method
  253. * @param {Db} db The Db instance on which to retrieve the index info.
  254. * @param {string} name The name of the collection.
  255. * @param {object} [options] Optional settings. See Db.prototype.indexInformation for a list of options.
  256. * @param {Db~resultCallback} [callback] The command result callback
  257. */
  258. function indexInformation(db, name, options, callback) {
  259. // If we specified full information
  260. const full = options['full'] == null ? false : options['full'];
  261. // Did the user destroy the topology
  262. if (db.serverConfig && db.serverConfig.isDestroyed())
  263. return callback(new MongoError('topology was destroyed'));
  264. // Process all the results from the index command and collection
  265. function processResults(indexes) {
  266. // Contains all the information
  267. let info = {};
  268. // Process all the indexes
  269. for (let i = 0; i < indexes.length; i++) {
  270. const index = indexes[i];
  271. // Let's unpack the object
  272. info[index.name] = [];
  273. for (let name in index.key) {
  274. info[index.name].push([name, index.key[name]]);
  275. }
  276. }
  277. return info;
  278. }
  279. // Get the list of indexes of the specified collection
  280. db.collection(name)
  281. .listIndexes(options)
  282. .toArray((err, indexes) => {
  283. if (err) return callback(toError(err));
  284. if (!Array.isArray(indexes)) return handleCallback(callback, null, []);
  285. if (full) return handleCallback(callback, null, indexes);
  286. handleCallback(callback, null, processResults(indexes));
  287. });
  288. }
  289. /**
  290. * Retrieve the current profiling information for MongoDB
  291. *
  292. * @method
  293. * @param {Db} db The Db instance on which to retrieve the profiling info.
  294. * @param {Object} [options] Optional settings. See Db.protoype.profilingInfo for a list of options.
  295. * @param {Db~resultCallback} [callback] The command result callback.
  296. * @deprecated Query the system.profile collection directly.
  297. */
  298. function profilingInfo(db, options, callback) {
  299. try {
  300. db.collection('system.profile')
  301. .find({}, options)
  302. .toArray(callback);
  303. } catch (err) {
  304. return callback(err, null);
  305. }
  306. }
  307. // Validate the database name
  308. function validateDatabaseName(databaseName) {
  309. if (typeof databaseName !== 'string')
  310. throw MongoError.create({ message: 'database name must be a string', driver: true });
  311. if (databaseName.length === 0)
  312. throw MongoError.create({ message: 'database name cannot be the empty string', driver: true });
  313. if (databaseName === '$external') return;
  314. const invalidChars = [' ', '.', '$', '/', '\\'];
  315. for (let i = 0; i < invalidChars.length; i++) {
  316. if (databaseName.indexOf(invalidChars[i]) !== -1)
  317. throw MongoError.create({
  318. message: "database names cannot contain the character '" + invalidChars[i] + "'",
  319. driver: true
  320. });
  321. }
  322. }
  323. /**
  324. * Create the command object for Db.prototype.createIndex.
  325. *
  326. * @param {Db} db The Db instance on which to create the command.
  327. * @param {string} name Name of the collection to create the index on.
  328. * @param {(string|object)} fieldOrSpec Defines the index.
  329. * @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
  330. * @return {Object} The insert command object.
  331. */
  332. function createCreateIndexCommand(db, name, fieldOrSpec, options) {
  333. const indexParameters = parseIndexOptions(fieldOrSpec);
  334. const fieldHash = indexParameters.fieldHash;
  335. // Generate the index name
  336. const indexName = typeof options.name === 'string' ? options.name : indexParameters.name;
  337. const selector = {
  338. ns: db.s.namespace.withCollection(name).toString(),
  339. key: fieldHash,
  340. name: indexName
  341. };
  342. // Ensure we have a correct finalUnique
  343. const finalUnique = options == null || 'object' === typeof options ? false : options;
  344. // Set up options
  345. options = options == null || typeof options === 'boolean' ? {} : options;
  346. // Add all the options
  347. const keysToOmit = Object.keys(selector);
  348. for (let optionName in options) {
  349. if (keysToOmit.indexOf(optionName) === -1) {
  350. selector[optionName] = options[optionName];
  351. }
  352. }
  353. if (selector['unique'] == null) selector['unique'] = finalUnique;
  354. // Remove any write concern operations
  355. const removeKeys = ['w', 'wtimeout', 'j', 'fsync', 'readPreference', 'session'];
  356. for (let i = 0; i < removeKeys.length; i++) {
  357. delete selector[removeKeys[i]];
  358. }
  359. // Return the command creation selector
  360. return selector;
  361. }
  362. /**
  363. * Create index using the createIndexes command.
  364. *
  365. * @param {Db} db The Db instance on which to execute the command.
  366. * @param {string} name Name of the collection to create the index on.
  367. * @param {(string|object)} fieldOrSpec Defines the index.
  368. * @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
  369. * @param {Db~resultCallback} [callback] The command result callback.
  370. */
  371. function createIndexUsingCreateIndexes(db, name, fieldOrSpec, options, callback) {
  372. // Build the index
  373. const indexParameters = parseIndexOptions(fieldOrSpec);
  374. // Generate the index name
  375. const indexName = typeof options.name === 'string' ? options.name : indexParameters.name;
  376. // Set up the index
  377. const indexes = [{ name: indexName, key: indexParameters.fieldHash }];
  378. // merge all the options
  379. const keysToOmit = Object.keys(indexes[0]).concat([
  380. 'writeConcern',
  381. 'w',
  382. 'wtimeout',
  383. 'j',
  384. 'fsync',
  385. 'readPreference',
  386. 'session'
  387. ]);
  388. for (let optionName in options) {
  389. if (keysToOmit.indexOf(optionName) === -1) {
  390. indexes[0][optionName] = options[optionName];
  391. }
  392. }
  393. // Get capabilities
  394. const capabilities = db.s.topology.capabilities();
  395. // Did the user pass in a collation, check if our write server supports it
  396. if (indexes[0].collation && capabilities && !capabilities.commandsTakeCollation) {
  397. // Create a new error
  398. const error = new MongoError('server/primary/mongos does not support collation');
  399. error.code = 67;
  400. // Return the error
  401. return callback(error);
  402. }
  403. // Create command, apply write concern to command
  404. const cmd = applyWriteConcern({ createIndexes: name, indexes }, { db }, options);
  405. // ReadPreference primary
  406. options.readPreference = ReadPreference.PRIMARY;
  407. // Build the command
  408. executeCommand(db, cmd, options, (err, result) => {
  409. if (err) return handleCallback(callback, err, null);
  410. if (result.ok === 0) return handleCallback(callback, toError(result), null);
  411. // Return the indexName for backward compatibility
  412. handleCallback(callback, null, indexName);
  413. });
  414. }
  415. module.exports = {
  416. createListener,
  417. createIndex,
  418. ensureIndex,
  419. evaluate,
  420. executeCommand,
  421. executeDbAdminCommand,
  422. indexInformation,
  423. profilingInfo,
  424. validateDatabaseName
  425. };