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.

794 lines
21 KiB

  1. /*!
  2. * Module dependencies.
  3. */
  4. var utils = require('./utils');
  5. var EventEmitter = require('events').EventEmitter;
  6. var driver = global.MONGOOSE_DRIVER_PATH || './drivers/node-mongodb-native';
  7. var Schema = require('./schema');
  8. var Collection = require(driver + '/collection');
  9. var STATES = require('./connectionstate');
  10. var MongooseError = require('./error');
  11. var muri = require('muri');
  12. var PromiseProvider = require('./promise_provider');
  13. /*!
  14. * Protocol prefix regexp.
  15. *
  16. * @api private
  17. */
  18. var rgxProtocol = /^(?:.)+:\/\//;
  19. /*!
  20. * A list of authentication mechanisms that don't require a password for authentication.
  21. * This is used by the authMechanismDoesNotRequirePassword method.
  22. *
  23. * @api private
  24. */
  25. var authMechanismsWhichDontRequirePassword = [
  26. 'MONGODB-X509'
  27. ];
  28. /**
  29. * Connection constructor
  30. *
  31. * For practical reasons, a Connection equals a Db.
  32. *
  33. * @param {Mongoose} base a mongoose instance
  34. * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
  35. * @event `connecting`: Emitted when `connection.{open,openSet}()` is executed on this connection.
  36. * @event `connected`: Emitted when this connection successfully connects to the db. May be emitted _multiple_ times in `reconnected` scenarios.
  37. * @event `open`: Emitted after we `connected` and `onOpen` is executed on all of this connections models.
  38. * @event `disconnecting`: Emitted when `connection.close()` was executed.
  39. * @event `disconnected`: Emitted after getting disconnected from the db.
  40. * @event `close`: Emitted after we `disconnected` and `onClose` executed on all of this connections models.
  41. * @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successfull connection.
  42. * @event `error`: Emitted when an error occurs on this connection.
  43. * @event `fullsetup`: Emitted in a replica-set scenario, when primary and at least one seconaries specified in the connection string are connected.
  44. * @event `all`: Emitted in a replica-set scenario, when all nodes specified in the connection string are connected.
  45. * @api public
  46. */
  47. function Connection(base) {
  48. this.base = base;
  49. this.collections = {};
  50. this.models = {};
  51. this.config = {autoIndex: true};
  52. this.replica = false;
  53. this.hosts = null;
  54. this.host = null;
  55. this.port = null;
  56. this.user = null;
  57. this.pass = null;
  58. this.name = null;
  59. this.options = null;
  60. this.otherDbs = [];
  61. this._readyState = STATES.disconnected;
  62. this._closeCalled = false;
  63. this._hasOpened = false;
  64. }
  65. /*!
  66. * Inherit from EventEmitter
  67. */
  68. Connection.prototype.__proto__ = EventEmitter.prototype;
  69. /**
  70. * Connection ready state
  71. *
  72. * - 0 = disconnected
  73. * - 1 = connected
  74. * - 2 = connecting
  75. * - 3 = disconnecting
  76. *
  77. * Each state change emits its associated event name.
  78. *
  79. * ####Example
  80. *
  81. * conn.on('connected', callback);
  82. * conn.on('disconnected', callback);
  83. *
  84. * @property readyState
  85. * @api public
  86. */
  87. Object.defineProperty(Connection.prototype, 'readyState', {
  88. get: function() {
  89. return this._readyState;
  90. },
  91. set: function(val) {
  92. if (!(val in STATES)) {
  93. throw new Error('Invalid connection state: ' + val);
  94. }
  95. if (this._readyState !== val) {
  96. this._readyState = val;
  97. // loop over the otherDbs on this connection and change their state
  98. for (var i = 0; i < this.otherDbs.length; i++) {
  99. this.otherDbs[i].readyState = val;
  100. }
  101. if (STATES.connected === val) {
  102. this._hasOpened = true;
  103. }
  104. this.emit(STATES[val]);
  105. }
  106. }
  107. });
  108. /**
  109. * A hash of the collections associated with this connection
  110. *
  111. * @property collections
  112. */
  113. Connection.prototype.collections;
  114. /**
  115. * The mongodb.Db instance, set when the connection is opened
  116. *
  117. * @property db
  118. */
  119. Connection.prototype.db;
  120. /**
  121. * A hash of the global options that are associated with this connection
  122. *
  123. * @property config
  124. */
  125. Connection.prototype.config;
  126. /**
  127. * Opens the connection to MongoDB.
  128. *
  129. * `options` is a hash with the following possible properties:
  130. *
  131. * config - passed to the connection config instance
  132. * db - passed to the connection db instance
  133. * server - passed to the connection server instance(s)
  134. * replset - passed to the connection ReplSet instance
  135. * user - username for authentication
  136. * pass - password for authentication
  137. * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate)
  138. *
  139. * ####Notes:
  140. *
  141. * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden.
  142. * Mongoose defaults the server `auto_reconnect` options to true which can be overridden.
  143. * See the node-mongodb-native driver instance for options that it understands.
  144. *
  145. * _Options passed take precedence over options included in connection strings._
  146. *
  147. * @param {String} connection_string mongodb://uri or the host to which you are connecting
  148. * @param {String} [database] database name
  149. * @param {Number} [port] database port
  150. * @param {Object} [options] options
  151. * @param {Function} [callback]
  152. * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native
  153. * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate
  154. * @api public
  155. */
  156. Connection.prototype.open = function(host, database, port, options, callback) {
  157. var parsed;
  158. if (typeof database === 'string') {
  159. switch (arguments.length) {
  160. case 2:
  161. port = 27017;
  162. break;
  163. case 3:
  164. switch (typeof port) {
  165. case 'function':
  166. callback = port;
  167. port = 27017;
  168. break;
  169. case 'object':
  170. options = port;
  171. port = 27017;
  172. break;
  173. }
  174. break;
  175. case 4:
  176. if (typeof options === 'function') {
  177. callback = options;
  178. options = {};
  179. }
  180. }
  181. } else {
  182. switch (typeof database) {
  183. case 'function':
  184. callback = database;
  185. database = undefined;
  186. break;
  187. case 'object':
  188. options = database;
  189. database = undefined;
  190. callback = port;
  191. break;
  192. }
  193. if (!rgxProtocol.test(host)) {
  194. host = 'mongodb://' + host;
  195. }
  196. try {
  197. parsed = muri(host);
  198. } catch (err) {
  199. this.error(err, callback);
  200. return this;
  201. }
  202. database = parsed.db;
  203. host = parsed.hosts[0].host || parsed.hosts[0].ipc;
  204. port = parsed.hosts[0].port || 27017;
  205. }
  206. this.options = this.parseOptions(options, parsed && parsed.options);
  207. // make sure we can open
  208. if (STATES.disconnected !== this.readyState) {
  209. var err = new Error('Trying to open unclosed connection.');
  210. err.state = this.readyState;
  211. this.error(err, callback);
  212. return this;
  213. }
  214. if (!host) {
  215. this.error(new Error('Missing hostname.'), callback);
  216. return this;
  217. }
  218. if (!database) {
  219. this.error(new Error('Missing database name.'), callback);
  220. return this;
  221. }
  222. // authentication
  223. if (this.optionsProvideAuthenticationData(options)) {
  224. this.user = options.user;
  225. this.pass = options.pass;
  226. } else if (parsed && parsed.auth) {
  227. this.user = parsed.auth.user;
  228. this.pass = parsed.auth.pass;
  229. // Check hostname for user/pass
  230. } else if (/@/.test(host) && /:/.test(host.split('@')[0])) {
  231. host = host.split('@');
  232. var auth = host.shift().split(':');
  233. host = host.pop();
  234. this.user = auth[0];
  235. this.pass = auth[1];
  236. } else {
  237. this.user = this.pass = undefined;
  238. }
  239. // global configuration options
  240. if (options && options.config) {
  241. this.config.autoIndex = options.config.autoIndex !== false;
  242. }
  243. this.name = database;
  244. this.host = host;
  245. this.port = port;
  246. var _this = this;
  247. var Promise = PromiseProvider.get();
  248. var promise = new Promise.ES6(function(resolve, reject) {
  249. _this._open(true, function(error) {
  250. callback && callback(error);
  251. if (error) {
  252. reject(error);
  253. if (!callback && !promise.$hasHandler) {
  254. _this.emit('error', error);
  255. }
  256. return;
  257. }
  258. resolve();
  259. });
  260. });
  261. return promise;
  262. };
  263. /**
  264. * Opens the connection to a replica set.
  265. *
  266. * ####Example:
  267. *
  268. * var db = mongoose.createConnection();
  269. * db.openSet("mongodb://user:pwd@localhost:27020/testing,mongodb://example.com:27020,mongodb://localhost:27019");
  270. *
  271. * The database name and/or auth need only be included in one URI.
  272. * The `options` is a hash which is passed to the internal driver connection object.
  273. *
  274. * Valid `options`
  275. *
  276. * db - passed to the connection db instance
  277. * server - passed to the connection server instance(s)
  278. * replset - passed to the connection ReplSetServer instance
  279. * user - username for authentication
  280. * pass - password for authentication
  281. * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate)
  282. * mongos - Boolean - if true, enables High Availability support for mongos
  283. *
  284. * _Options passed take precedence over options included in connection strings._
  285. *
  286. * ####Notes:
  287. *
  288. * _If connecting to multiple mongos servers, set the `mongos` option to true._
  289. *
  290. * conn.open('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb);
  291. *
  292. * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden.
  293. * Mongoose defaults the server `auto_reconnect` options to true which can be overridden.
  294. * See the node-mongodb-native driver instance for options that it understands.
  295. *
  296. * _Options passed take precedence over options included in connection strings._
  297. *
  298. * @param {String} uris comma-separated mongodb:// `URI`s
  299. * @param {String} [database] database name if not included in `uris`
  300. * @param {Object} [options] passed to the internal driver
  301. * @param {Function} [callback]
  302. * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native
  303. * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate
  304. * @api public
  305. */
  306. Connection.prototype.openSet = function(uris, database, options, callback) {
  307. if (!rgxProtocol.test(uris)) {
  308. uris = 'mongodb://' + uris;
  309. }
  310. switch (arguments.length) {
  311. case 3:
  312. switch (typeof database) {
  313. case 'string':
  314. this.name = database;
  315. break;
  316. case 'object':
  317. callback = options;
  318. options = database;
  319. database = null;
  320. break;
  321. }
  322. if (typeof options === 'function') {
  323. callback = options;
  324. options = {};
  325. }
  326. break;
  327. case 2:
  328. switch (typeof database) {
  329. case 'string':
  330. this.name = database;
  331. break;
  332. case 'function':
  333. callback = database;
  334. database = null;
  335. break;
  336. case 'object':
  337. options = database;
  338. database = null;
  339. break;
  340. }
  341. }
  342. var parsed;
  343. try {
  344. parsed = muri(uris);
  345. } catch (err) {
  346. this.error(err, callback);
  347. return this;
  348. }
  349. if (!this.name) {
  350. this.name = parsed.db;
  351. }
  352. this.hosts = parsed.hosts;
  353. this.options = this.parseOptions(options, parsed && parsed.options);
  354. this.replica = true;
  355. if (!this.name) {
  356. this.error(new Error('No database name provided for replica set'), callback);
  357. return this;
  358. }
  359. // authentication
  360. if (this.optionsProvideAuthenticationData(options)) {
  361. this.user = options.user;
  362. this.pass = options.pass;
  363. } else if (parsed && parsed.auth) {
  364. this.user = parsed.auth.user;
  365. this.pass = parsed.auth.pass;
  366. } else {
  367. this.user = this.pass = undefined;
  368. }
  369. // global configuration options
  370. if (options && options.config) {
  371. this.config.autoIndex = options.config.autoIndex !== false;
  372. }
  373. var _this = this;
  374. var Promise = PromiseProvider.get();
  375. var promise = new Promise.ES6(function(resolve, reject) {
  376. _this._open(true, function(error) {
  377. callback && callback(error);
  378. if (error) {
  379. reject(error);
  380. if (!callback && !promise.$hasHandler) {
  381. _this.emit('error', error);
  382. }
  383. return;
  384. }
  385. resolve();
  386. });
  387. });
  388. return promise;
  389. };
  390. /**
  391. * error
  392. *
  393. * Graceful error handling, passes error to callback
  394. * if available, else emits error on the connection.
  395. *
  396. * @param {Error} err
  397. * @param {Function} callback optional
  398. * @api private
  399. */
  400. Connection.prototype.error = function(err, callback) {
  401. if (callback) {
  402. return callback(err);
  403. }
  404. this.emit('error', err);
  405. };
  406. /**
  407. * Handles opening the connection with the appropriate method based on connection type.
  408. *
  409. * @param {Function} callback
  410. * @api private
  411. */
  412. Connection.prototype._open = function(emit, callback) {
  413. this.readyState = STATES.connecting;
  414. this._closeCalled = false;
  415. var _this = this;
  416. var method = this.replica
  417. ? 'doOpenSet'
  418. : 'doOpen';
  419. // open connection
  420. this[method](function(err) {
  421. if (err) {
  422. _this.readyState = STATES.disconnected;
  423. if (_this._hasOpened) {
  424. if (callback) {
  425. callback(err);
  426. }
  427. } else {
  428. _this.error(err, emit && callback);
  429. }
  430. return;
  431. }
  432. _this.onOpen(callback);
  433. });
  434. };
  435. /**
  436. * Called when the connection is opened
  437. *
  438. * @api private
  439. */
  440. Connection.prototype.onOpen = function(callback) {
  441. var _this = this;
  442. function open(err, isAuth) {
  443. if (err) {
  444. _this.readyState = isAuth ? STATES.unauthorized : STATES.disconnected;
  445. _this.error(err, callback);
  446. return;
  447. }
  448. _this.readyState = STATES.connected;
  449. // avoid having the collection subscribe to our event emitter
  450. // to prevent 0.3 warning
  451. for (var i in _this.collections) {
  452. if (utils.object.hasOwnProperty(_this.collections, i)) {
  453. _this.collections[i].onOpen();
  454. }
  455. }
  456. callback && callback();
  457. _this.emit('open');
  458. }
  459. // re-authenticate if we're not already connected #3871
  460. if (this._readyState !== STATES.connected && this.shouldAuthenticate()) {
  461. _this.db.authenticate(_this.user, _this.pass, _this.options.auth, function(err) {
  462. open(err, true);
  463. });
  464. } else {
  465. open();
  466. }
  467. };
  468. /**
  469. * Closes the connection
  470. *
  471. * @param {Function} [callback] optional
  472. * @return {Connection} self
  473. * @api public
  474. */
  475. Connection.prototype.close = function(callback) {
  476. var _this = this;
  477. var Promise = PromiseProvider.get();
  478. return new Promise.ES6(function(resolve, reject) {
  479. _this._close(function(error) {
  480. callback && callback(error);
  481. if (error) {
  482. reject(error);
  483. return;
  484. }
  485. resolve();
  486. });
  487. });
  488. };
  489. /**
  490. * Handles closing the connection
  491. *
  492. * @param {Function} callback
  493. * @api private
  494. */
  495. Connection.prototype._close = function(callback) {
  496. var _this = this;
  497. this._closeCalled = true;
  498. switch (this.readyState) {
  499. case 0: // disconnected
  500. callback && callback();
  501. break;
  502. case 1: // connected
  503. case 4: // unauthorized
  504. this.readyState = STATES.disconnecting;
  505. this.doClose(function(err) {
  506. if (err) {
  507. _this.error(err, callback);
  508. } else {
  509. _this.onClose();
  510. callback && callback();
  511. }
  512. });
  513. break;
  514. case 2: // connecting
  515. this.once('open', function() {
  516. _this.close(callback);
  517. });
  518. break;
  519. case 3: // disconnecting
  520. if (!callback) {
  521. break;
  522. }
  523. this.once('close', function() {
  524. callback();
  525. });
  526. break;
  527. }
  528. return this;
  529. };
  530. /**
  531. * Called when the connection closes
  532. *
  533. * @api private
  534. */
  535. Connection.prototype.onClose = function() {
  536. this.readyState = STATES.disconnected;
  537. // avoid having the collection subscribe to our event emitter
  538. // to prevent 0.3 warning
  539. for (var i in this.collections) {
  540. if (utils.object.hasOwnProperty(this.collections, i)) {
  541. this.collections[i].onClose();
  542. }
  543. }
  544. this.emit('close');
  545. };
  546. /**
  547. * Retrieves a collection, creating it if not cached.
  548. *
  549. * Not typically needed by applications. Just talk to your collection through your model.
  550. *
  551. * @param {String} name of the collection
  552. * @param {Object} [options] optional collection options
  553. * @return {Collection} collection instance
  554. * @api public
  555. */
  556. Connection.prototype.collection = function(name, options) {
  557. if (!(name in this.collections)) {
  558. this.collections[name] = new Collection(name, this, options);
  559. }
  560. return this.collections[name];
  561. };
  562. /**
  563. * Defines or retrieves a model.
  564. *
  565. * var mongoose = require('mongoose');
  566. * var db = mongoose.createConnection(..);
  567. * db.model('Venue', new Schema(..));
  568. * var Ticket = db.model('Ticket', new Schema(..));
  569. * var Venue = db.model('Venue');
  570. *
  571. * _When no `collection` argument is passed, Mongoose produces a collection name by passing the model `name` to the [utils.toCollectionName](#utils_exports.toCollectionName) method. This method pluralizes the name. If you don't like this behavior, either pass a collection name or set your schemas collection name option._
  572. *
  573. * ####Example:
  574. *
  575. * var schema = new Schema({ name: String }, { collection: 'actor' });
  576. *
  577. * // or
  578. *
  579. * schema.set('collection', 'actor');
  580. *
  581. * // or
  582. *
  583. * var collectionName = 'actor'
  584. * var M = conn.model('Actor', schema, collectionName)
  585. *
  586. * @param {String} name the model name
  587. * @param {Schema} [schema] a schema. necessary when defining a model
  588. * @param {String} [collection] name of mongodb collection (optional) if not given it will be induced from model name
  589. * @see Mongoose#model #index_Mongoose-model
  590. * @return {Model} The compiled model
  591. * @api public
  592. */
  593. Connection.prototype.model = function(name, schema, collection) {
  594. // collection name discovery
  595. if (typeof schema === 'string') {
  596. collection = schema;
  597. schema = false;
  598. }
  599. if (utils.isObject(schema) && !schema.instanceOfSchema) {
  600. schema = new Schema(schema);
  601. }
  602. if (this.models[name] && !collection) {
  603. // model exists but we are not subclassing with custom collection
  604. if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) {
  605. throw new MongooseError.OverwriteModelError(name);
  606. }
  607. return this.models[name];
  608. }
  609. var opts = {cache: false, connection: this};
  610. var model;
  611. if (schema && schema.instanceOfSchema) {
  612. // compile a model
  613. model = this.base.model(name, schema, collection, opts);
  614. // only the first model with this name is cached to allow
  615. // for one-offs with custom collection names etc.
  616. if (!this.models[name]) {
  617. this.models[name] = model;
  618. }
  619. model.init();
  620. return model;
  621. }
  622. if (this.models[name] && collection) {
  623. // subclassing current model with alternate collection
  624. model = this.models[name];
  625. schema = model.prototype.schema;
  626. var sub = model.__subclass(this, schema, collection);
  627. // do not cache the sub model
  628. return sub;
  629. }
  630. // lookup model in mongoose module
  631. model = this.base.models[name];
  632. if (!model) {
  633. throw new MongooseError.MissingSchemaError(name);
  634. }
  635. if (this === model.prototype.db
  636. && (!collection || collection === model.collection.name)) {
  637. // model already uses this connection.
  638. // only the first model with this name is cached to allow
  639. // for one-offs with custom collection names etc.
  640. if (!this.models[name]) {
  641. this.models[name] = model;
  642. }
  643. return model;
  644. }
  645. this.models[name] = model.__subclass(this, schema, collection);
  646. return this.models[name];
  647. };
  648. /**
  649. * Returns an array of model names created on this connection.
  650. * @api public
  651. * @return {Array}
  652. */
  653. Connection.prototype.modelNames = function() {
  654. return Object.keys(this.models);
  655. };
  656. /**
  657. * @brief Returns if the connection requires authentication after it is opened. Generally if a
  658. * username and password are both provided than authentication is needed, but in some cases a
  659. * password is not required.
  660. * @api private
  661. * @return {Boolean} true if the connection should be authenticated after it is opened, otherwise false.
  662. */
  663. Connection.prototype.shouldAuthenticate = function() {
  664. return (this.user !== null && this.user !== void 0) &&
  665. ((this.pass !== null || this.pass !== void 0) || this.authMechanismDoesNotRequirePassword());
  666. };
  667. /**
  668. * @brief Returns a boolean value that specifies if the current authentication mechanism needs a
  669. * password to authenticate according to the auth objects passed into the open/openSet methods.
  670. * @api private
  671. * @return {Boolean} true if the authentication mechanism specified in the options object requires
  672. * a password, otherwise false.
  673. */
  674. Connection.prototype.authMechanismDoesNotRequirePassword = function() {
  675. if (this.options && this.options.auth) {
  676. return authMechanismsWhichDontRequirePassword.indexOf(this.options.auth.authMechanism) >= 0;
  677. }
  678. return true;
  679. };
  680. /**
  681. * @brief Returns a boolean value that specifies if the provided objects object provides enough
  682. * data to authenticate with. Generally this is true if the username and password are both specified
  683. * but in some authentication methods, a password is not required for authentication so only a username
  684. * is required.
  685. * @param {Object} [options] the options object passed into the open/openSet methods.
  686. * @api private
  687. * @return {Boolean} true if the provided options object provides enough data to authenticate with,
  688. * otherwise false.
  689. */
  690. Connection.prototype.optionsProvideAuthenticationData = function(options) {
  691. return (options) &&
  692. (options.user) &&
  693. ((options.pass) || this.authMechanismDoesNotRequirePassword());
  694. };
  695. /*!
  696. * Module exports.
  697. */
  698. Connection.STATES = STATES;
  699. module.exports = Connection;