db_ops.js 16 KB

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