db_ops.js 16 KB

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