UNPKG

30.3 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const EventEmitter = require('events').EventEmitter;
8const Schema = require('./schema');
9const Collection = require('./driver').get().Collection;
10const STATES = require('./connectionstate');
11const MongooseError = require('./error');
12const PromiseProvider = require('./promise_provider');
13const applyPlugins = require('./helpers/schema/applyPlugins');
14const get = require('./helpers/get');
15const mongodb = require('mongodb');
16const utils = require('./utils');
17
18const parseConnectionString = require('mongodb-core').parseConnectionString;
19
20/*!
21 * A list of authentication mechanisms that don't require a password for authentication.
22 * This is used by the authMechanismDoesNotRequirePassword method.
23 *
24 * @api private
25 */
26const noPasswordAuthMechanisms = [
27 'MONGODB-X509'
28];
29
30/**
31 * Connection constructor
32 *
33 * For practical reasons, a Connection equals a Db.
34 *
35 * @param {Mongoose} base a mongoose instance
36 * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
37 * @event `connecting`: Emitted when `connection.openUri()` is executed on this connection.
38 * @event `connected`: Emitted when this connection successfully connects to the db. May be emitted _multiple_ times in `reconnected` scenarios.
39 * @event `open`: Emitted after we `connected` and `onOpen` is executed on all of this connections models.
40 * @event `disconnecting`: Emitted when `connection.close()` was executed.
41 * @event `disconnected`: Emitted after getting disconnected from the db.
42 * @event `close`: Emitted after we `disconnected` and `onClose` executed on all of this connections models.
43 * @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successful connection.
44 * @event `error`: Emitted when an error occurs on this connection.
45 * @event `fullsetup`: Emitted after the driver has connected to primary and all secondaries if specified in the connection string.
46 * @api public
47 */
48
49function Connection(base) {
50 this.base = base;
51 this.collections = {};
52 this.models = {};
53 this.config = {autoIndex: true};
54 this.replica = false;
55 this.options = null;
56 this.otherDbs = []; // FIXME: To be replaced with relatedDbs
57 this.relatedDbs = {}; // Hashmap of other dbs that share underlying connection
58 this.states = STATES;
59 this._readyState = STATES.disconnected;
60 this._closeCalled = false;
61 this._hasOpened = false;
62 this.plugins = [];
63}
64
65/*!
66 * Inherit from EventEmitter
67 */
68
69Connection.prototype.__proto__ = EventEmitter.prototype;
70
71/**
72 * Connection ready state
73 *
74 * - 0 = disconnected
75 * - 1 = connected
76 * - 2 = connecting
77 * - 3 = disconnecting
78 *
79 * Each state change emits its associated event name.
80 *
81 * ####Example
82 *
83 * conn.on('connected', callback);
84 * conn.on('disconnected', callback);
85 *
86 * @property readyState
87 * @memberOf Connection
88 * @instance
89 * @api public
90 */
91
92Object.defineProperty(Connection.prototype, 'readyState', {
93 get: function() {
94 return this._readyState;
95 },
96 set: function(val) {
97 if (!(val in STATES)) {
98 throw new Error('Invalid connection state: ' + val);
99 }
100
101 if (this._readyState !== val) {
102 this._readyState = val;
103 // [legacy] loop over the otherDbs on this connection and change their state
104 for (let i = 0; i < this.otherDbs.length; i++) {
105 this.otherDbs[i].readyState = val;
106 }
107
108 // loop over relatedDbs on this connection and change their state
109 for (const k in this.relatedDbs) {
110 this.relatedDbs[k].readyState = val;
111 }
112
113 if (STATES.connected === val) {
114 this._hasOpened = true;
115 }
116
117 this.emit(STATES[val]);
118 }
119 }
120});
121
122/**
123 * Gets the value of the option `key`. Equivalent to `conn.options[key]`
124 *
125 * ####Example:
126 *
127 * conn.get('test'); // returns the 'test' value
128 *
129 * @param {String} key
130 * @method get
131 * @api public
132 */
133
134Connection.prototype.get = function(key) {
135 return get(this.options, key);
136};
137
138/**
139 * Sets the value of the option `key`. Equivalent to `conn.options[key] = val`
140 *
141 * Supported options include:
142 *
143 * - `maxTimeMS`: Set [`maxTimeMS`](/docs/api.html#query_Query-maxTimeMS) for all queries on this connection.
144 * - `useFindAndModify`: Set to `false` to work around the [`findAndModify()` deprecation warning](/docs/deprecations.html#-findandmodify-)
145 *
146 * ####Example:
147 *
148 * conn.set('test', 'foo');
149 * conn.get('test'); // 'foo'
150 * conn.options.test; // 'foo'
151 *
152 * @param {String} key
153 * @param {Any} val
154 * @method set
155 * @api public
156 */
157
158Connection.prototype.set = function(key, val) {
159 this.options = this.options || {};
160 this.options[key] = val;
161 return val;
162};
163
164/**
165 * A hash of the collections associated with this connection
166 *
167 * @property collections
168 * @memberOf Connection
169 * @instance
170 * @api public
171 */
172
173Connection.prototype.collections;
174
175/**
176 * The name of the database this connection points to.
177 *
178 * ####Example
179 *
180 * mongoose.createConnection('mongodb://localhost:27017/mydb').name; // "mydb"
181 *
182 * @property name
183 * @memberOf Connection
184 * @instance
185 * @api public
186 */
187
188Connection.prototype.name;
189
190/**
191 * The plugins that will be applied to all models created on this connection.
192 *
193 * ####Example:
194 *
195 * const db = mongoose.createConnection('mongodb://localhost:27017/mydb');
196 * db.plugin(() => console.log('Applied'));
197 * db.plugins.length; // 1
198 *
199 * db.model('Test', new Schema({})); // Prints "Applied"
200 *
201 * @property plugins
202 * @memberOf Connection
203 * @instance
204 * @api public
205 */
206
207Object.defineProperty(Connection.prototype, 'plugins', {
208 configurable: false,
209 enumerable: true,
210 writable: true
211});
212
213/**
214 * The host name portion of the URI. If multiple hosts, such as a replica set,
215 * this will contain the first host name in the URI
216 *
217 * ####Example
218 *
219 * mongoose.createConnection('mongodb://localhost:27017/mydb').host; // "localhost"
220 *
221 * @property host
222 * @memberOf Connection
223 * @instance
224 * @api public
225 */
226
227Object.defineProperty(Connection.prototype, 'host', {
228 configurable: true,
229 enumerable: true,
230 writable: true
231});
232
233/**
234 * The port portion of the URI. If multiple hosts, such as a replica set,
235 * this will contain the port from the first host name in the URI.
236 *
237 * ####Example
238 *
239 * mongoose.createConnection('mongodb://localhost:27017/mydb').port; // 27017
240 *
241 * @property port
242 * @memberOf Connection
243 * @instance
244 * @api public
245 */
246
247Object.defineProperty(Connection.prototype, 'port', {
248 configurable: true,
249 enumerable: true,
250 writable: true
251});
252
253/**
254 * The username specified in the URI
255 *
256 * ####Example
257 *
258 * mongoose.createConnection('mongodb://val:psw@localhost:27017/mydb').user; // "val"
259 *
260 * @property user
261 * @memberOf Connection
262 * @instance
263 * @api public
264 */
265
266Object.defineProperty(Connection.prototype, 'user', {
267 configurable: true,
268 enumerable: true,
269 writable: true
270});
271
272/**
273 * The password specified in the URI
274 *
275 * ####Example
276 *
277 * mongoose.createConnection('mongodb://val:psw@localhost:27017/mydb').pass; // "psw"
278 *
279 * @property pass
280 * @memberOf Connection
281 * @instance
282 * @api public
283 */
284
285Object.defineProperty(Connection.prototype, 'pass', {
286 configurable: true,
287 enumerable: true,
288 writable: true
289});
290
291/**
292 * The mongodb.Db instance, set when the connection is opened
293 *
294 * @property db
295 * @memberOf Connection
296 * @instance
297 * @api public
298 */
299
300Connection.prototype.db;
301
302/**
303 * A hash of the global options that are associated with this connection
304 *
305 * @property config
306 * @memberOf Connection
307 * @instance
308 * @api public
309 */
310
311Connection.prototype.config;
312
313/**
314 * Helper for `createCollection()`. Will explicitly create the given collection
315 * with specified options. Used to create [capped collections](https://docs.mongodb.com/manual/core/capped-collections/)
316 * and [views](https://docs.mongodb.com/manual/core/views/) from mongoose.
317 *
318 * Options are passed down without modification to the [MongoDB driver's `createCollection()` function](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection)
319 *
320 * @method createCollection
321 * @param {string} collection The collection to create
322 * @param {Object} [options] see [MongoDB driver docs](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection)
323 * @param {Function} [callback]
324 * @return {Promise}
325 * @api public
326 */
327
328Connection.prototype.createCollection = _wrapConnHelper(function createCollection(collection, options, cb) {
329 if (typeof options === 'function') {
330 cb = options;
331 options = {};
332 }
333 this.db.createCollection(collection, options, cb);
334});
335
336/**
337 * _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://docs.mongodb.com/manual/release-notes/3.6/#client-sessions)
338 * for benefits like causal consistency, [retryable writes](https://docs.mongodb.com/manual/core/retryable-writes/),
339 * and [transactions](http://thecodebarbarian.com/a-node-js-perspective-on-mongodb-4-transactions.html).
340 *
341 * ####Example:
342 *
343 * const session = await conn.startSession();
344 * let doc = await Person.findOne({ name: 'Ned Stark' }, null, { session });
345 * await doc.remove();
346 * // `doc` will always be null, even if reading from a replica set
347 * // secondary. Without causal consistency, it is possible to
348 * // get a doc back from the below query if the query reads from a
349 * // secondary that is experiencing replication lag.
350 * doc = await Person.findOne({ name: 'Ned Stark' }, null, { session, readPreference: 'secondary' });
351 *
352 *
353 * @method startSession
354 * @param {Object} [options] see the [mongodb driver options](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html#startSession)
355 * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
356 * @param {Function} [callback]
357 * @return {Promise<ClientSession>} promise that resolves to a MongoDB driver `ClientSession`
358 * @api public
359 */
360
361Connection.prototype.startSession = _wrapConnHelper(function startSession(options, cb) {
362 if (typeof options === 'function') {
363 cb = options;
364 options = null;
365 }
366 const session = this.client.startSession(options);
367 cb(null, session);
368});
369
370/**
371 * Helper for `dropCollection()`. Will delete the given collection, including
372 * all documents and indexes.
373 *
374 * @method dropCollection
375 * @param {string} collection The collection to delete
376 * @param {Function} [callback]
377 * @return {Promise}
378 * @api public
379 */
380
381Connection.prototype.dropCollection = _wrapConnHelper(function dropCollection(collection, cb) {
382 this.db.dropCollection(collection, cb);
383});
384
385/**
386 * Helper for `dropDatabase()`. Deletes the given database, including all
387 * collections, documents, and indexes.
388 *
389 * ####Example:
390 *
391 * const conn = mongoose.createConnection('mongodb://localhost:27017/mydb');
392 * // Deletes the entire 'mydb' database
393 * await conn.dropDatabase();
394 *
395 * @method dropDatabase
396 * @param {Function} [callback]
397 * @return {Promise}
398 * @api public
399 */
400
401Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) {
402 // If `dropDatabase()` is called, this model's collection will not be
403 // init-ed. It is sufficiently common to call `dropDatabase()` after
404 // `mongoose.connect()` but before creating models that we want to
405 // support this. See gh-6967
406 for (const name of Object.keys(this.models)) {
407 delete this.models[name].$init;
408 }
409 this.db.dropDatabase(cb);
410});
411
412/*!
413 * ignore
414 */
415
416function _wrapConnHelper(fn) {
417 return function() {
418 const cb = arguments.length > 0 ? arguments[arguments.length - 1] : null;
419 const argsWithoutCb = typeof cb === 'function' ?
420 Array.prototype.slice.call(arguments, 0, arguments.length - 1) :
421 Array.prototype.slice.call(arguments);
422 return utils.promiseOrCallback(cb, cb => {
423 if (this.readyState !== STATES.connected) {
424 this.once('open', function() {
425 fn.apply(this, argsWithoutCb.concat([cb]));
426 });
427 } else {
428 fn.apply(this, argsWithoutCb.concat([cb]));
429 }
430 });
431 };
432}
433
434/**
435 * error
436 *
437 * Graceful error handling, passes error to callback
438 * if available, else emits error on the connection.
439 *
440 * @param {Error} err
441 * @param {Function} callback optional
442 * @api private
443 */
444
445Connection.prototype.error = function(err, callback) {
446 if (callback) {
447 callback(err);
448 return null;
449 }
450 if (this.listeners('error').length > 0) {
451 this.emit('error', err);
452 }
453 return Promise.reject(err);
454};
455
456/**
457 * Called when the connection is opened
458 *
459 * @api private
460 */
461
462Connection.prototype.onOpen = function() {
463 this.readyState = STATES.connected;
464
465 // avoid having the collection subscribe to our event emitter
466 // to prevent 0.3 warning
467 for (const i in this.collections) {
468 if (utils.object.hasOwnProperty(this.collections, i)) {
469 this.collections[i].onOpen();
470 }
471 }
472
473 this.emit('open');
474};
475
476/**
477 * Opens the connection with a URI using `MongoClient.connect()`.
478 *
479 * @param {String} uri The URI to connect with.
480 * @param {Object} [options] Passed on to http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect
481 * @param {Function} [callback]
482 * @returns {Connection} this
483 * @api public
484 */
485
486Connection.prototype.openUri = function(uri, options, callback) {
487 this.readyState = STATES.connecting;
488 this._closeCalled = false;
489
490 if (typeof options === 'function') {
491 callback = options;
492 options = null;
493 }
494
495 if (['string', 'number'].indexOf(typeof options) !== -1) {
496 throw new MongooseError('Mongoose 5.x no longer supports ' +
497 '`mongoose.connect(host, dbname, port)` or ' +
498 '`mongoose.createConnection(host, dbname, port)`. See ' +
499 'http://mongoosejs.com/docs/connections.html for supported connection syntax');
500 }
501
502 if (typeof uri !== 'string') {
503 throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
504 `string, got "${typeof uri}". Make sure the first parameter to ` +
505 '`mongoose.connect()` or `mongoose.createConnection()` is a string.');
506 }
507
508 const Promise = PromiseProvider.get();
509 const _this = this;
510
511 if (options) {
512 options = utils.clone(options);
513 const autoIndex = options.config && options.config.autoIndex != null ?
514 options.config.autoIndex :
515 options.autoIndex;
516 if (autoIndex != null) {
517 this.config.autoIndex = autoIndex !== false;
518 delete options.config;
519 delete options.autoIndex;
520 }
521
522 if ('autoCreate' in options) {
523 this.config.autoCreate = !!options.autoCreate;
524 delete options.autoCreate;
525 }
526 if ('useCreateIndex' in options) {
527 this.config.useCreateIndex = !!options.useCreateIndex;
528 delete options.useCreateIndex;
529 }
530
531 if ('useFindAndModify' in options) {
532 this.config.useFindAndModify = !!options.useFindAndModify;
533 delete options.useFindAndModify;
534 }
535
536 // Backwards compat
537 if (options.user || options.pass) {
538 options.auth = options.auth || {};
539 options.auth.user = options.user;
540 options.auth.password = options.pass;
541
542 this.user = options.user;
543 this.pass = options.pass;
544 }
545 delete options.user;
546 delete options.pass;
547
548 if (options.bufferCommands != null) {
549 options.bufferMaxEntries = 0;
550 this.config.bufferCommands = options.bufferCommands;
551 delete options.bufferCommands;
552 }
553
554 if (options.useMongoClient != null) {
555 handleUseMongoClient(options);
556 }
557 } else {
558 options = {};
559 }
560
561 this._connectionOptions = options;
562 const dbName = options.dbName;
563 if (dbName != null) {
564 this.$dbName = dbName;
565 }
566 delete options.dbName;
567
568 if (!('promiseLibrary' in options)) {
569 options.promiseLibrary = PromiseProvider.get();
570 }
571 if (!('useNewUrlParser' in options)) {
572 if ('useNewUrlParser' in this.base.options) {
573 options.useNewUrlParser = this.base.options.useNewUrlParser;
574 } else {
575 options.useNewUrlParser = false;
576 }
577 }
578
579 const parsePromise = new Promise((resolve, reject) => {
580 parseConnectionString(uri, options, (err, parsed) => {
581 if (err) {
582 return reject(err);
583 }
584 this.name = dbName != null ? dbName : get(parsed, 'auth.db', null);
585 this.host = get(parsed, 'hosts.0.host', 'localhost');
586 this.port = get(parsed, 'hosts.0.port', 27017);
587 this.user = this.user || get(parsed, 'auth.username');
588 this.pass = this.pass || get(parsed, 'auth.password');
589 resolve();
590 });
591 });
592
593 const promise = new Promise((resolve, reject) => {
594 const client = new mongodb.MongoClient(uri, options);
595 _this.client = client;
596 client.connect(function(error) {
597 if (error) {
598 _this.readyState = STATES.disconnected;
599 return reject(error);
600 }
601
602 const db = dbName != null ? client.db(dbName) : client.db();
603 _this.db = db;
604
605 // Backwards compat for mongoose 4.x
606 db.on('reconnect', function() {
607 // If we aren't disconnected, we assume this reconnect is due to a
608 // socket timeout. If there's no activity on a socket for
609 // `socketTimeoutMS`, the driver will attempt to reconnect and emit
610 // this event.
611 if (_this.readyState !== STATES.connected) {
612 _this.readyState = STATES.connected;
613 _this.emit('reconnect');
614 _this.emit('reconnected');
615 }
616 });
617 db.s.topology.on('reconnectFailed', function() {
618 _this.emit('reconnectFailed');
619 });
620 db.s.topology.on('left', function(data) {
621 _this.emit('left', data);
622 });
623 db.s.topology.on('joined', function(data) {
624 _this.emit('joined', data);
625 });
626 db.s.topology.on('fullsetup', function(data) {
627 _this.emit('fullsetup', data);
628 });
629 db.s.topology.on('attemptReconnect', function() {
630 _this.emit('attemptReconnect');
631 });
632 db.on('close', function() {
633 // Implicitly emits 'disconnected'
634 _this.readyState = STATES.disconnected;
635 });
636 client.on('left', function() {
637 if (_this.readyState === STATES.connected &&
638 get(db, 's.topology.s.coreTopology.s.replicaSetState.topologyType') === 'ReplicaSetNoPrimary') {
639 _this.readyState = STATES.disconnected;
640 }
641 });
642 db.on('timeout', function() {
643 _this.emit('timeout');
644 });
645
646 delete _this.then;
647 delete _this.catch;
648 _this.readyState = STATES.connected;
649
650 for (const i in _this.collections) {
651 if (utils.object.hasOwnProperty(_this.collections, i)) {
652 _this.collections[i].onOpen();
653 }
654 }
655
656 resolve(_this);
657 _this.emit('open');
658 });
659 });
660
661 this.$initialConnection = Promise.all([promise, parsePromise]).
662 then(res => res[0]).
663 catch(err => {
664 if (this.listeners('error').length > 0) {
665 process.nextTick(() => this.emit('error', err));
666 }
667 throw err;
668 });
669 this.then = function(resolve, reject) {
670 return this.$initialConnection.then(resolve, reject);
671 };
672 this.catch = function(reject) {
673 return this.$initialConnection.catch(reject);
674 };
675
676 if (callback != null) {
677 this.$initialConnection = this.$initialConnection.then(
678 () => callback(null, this),
679 err => callback(err)
680 );
681 }
682
683 return this;
684};
685
686/*!
687 * ignore
688 */
689
690const handleUseMongoClient = function handleUseMongoClient(options) {
691 console.warn('WARNING: The `useMongoClient` option is no longer ' +
692 'necessary in mongoose 5.x, please remove it.');
693 const stack = new Error().stack;
694 console.warn(stack.substr(stack.indexOf('\n') + 1));
695 delete options.useMongoClient;
696};
697
698/**
699 * Closes the connection
700 *
701 * @param {Boolean} [force] optional
702 * @param {Function} [callback] optional
703 * @return {Connection} self
704 * @api public
705 */
706
707Connection.prototype.close = function(force, callback) {
708 if (typeof force === 'function') {
709 callback = force;
710 force = false;
711 }
712
713 this.$wasForceClosed = !!force;
714
715 return utils.promiseOrCallback(callback, cb => {
716 this._close(force, cb);
717 });
718};
719
720/**
721 * Handles closing the connection
722 *
723 * @param {Boolean} force
724 * @param {Function} callback
725 * @api private
726 */
727Connection.prototype._close = function(force, callback) {
728 const _this = this;
729 this._closeCalled = true;
730
731 switch (this.readyState) {
732 case STATES.disconnected:
733 callback();
734 break;
735
736 case STATES.connected:
737 this.readyState = STATES.disconnecting;
738 this.doClose(force, function(err) {
739 if (err) {
740 return callback(err);
741 }
742 _this.onClose(force);
743 callback(null);
744 });
745
746 break;
747 case STATES.connecting:
748 this.once('open', function() {
749 _this.close(callback);
750 });
751 break;
752
753 case STATES.disconnecting:
754 this.once('close', function() {
755 callback();
756 });
757 break;
758 }
759
760 return this;
761};
762
763/**
764 * Called when the connection closes
765 *
766 * @api private
767 */
768
769Connection.prototype.onClose = function(force) {
770 this.readyState = STATES.disconnected;
771
772 // avoid having the collection subscribe to our event emitter
773 // to prevent 0.3 warning
774 for (const i in this.collections) {
775 if (utils.object.hasOwnProperty(this.collections, i)) {
776 this.collections[i].onClose(force);
777 }
778 }
779
780 this.emit('close', force);
781};
782
783/**
784 * Retrieves a collection, creating it if not cached.
785 *
786 * Not typically needed by applications. Just talk to your collection through your model.
787 *
788 * @param {String} name of the collection
789 * @param {Object} [options] optional collection options
790 * @return {Collection} collection instance
791 * @api public
792 */
793
794Connection.prototype.collection = function(name, options) {
795 options = options ? utils.clone(options) : {};
796 options.$wasForceClosed = this.$wasForceClosed;
797 if (!(name in this.collections)) {
798 this.collections[name] = new Collection(name, this, options);
799 }
800 return this.collections[name];
801};
802
803/**
804 * Declares a plugin executed on all schemas you pass to `conn.model()`
805 *
806 * Equivalent to calling `.plugin(fn)` on each schema you create.
807 *
808 * ####Example:
809 * const db = mongoose.createConnection('mongodb://localhost:27017/mydb');
810 * db.plugin(() => console.log('Applied'));
811 * db.plugins.length; // 1
812 *
813 * db.model('Test', new Schema({})); // Prints "Applied"
814 *
815 * @param {Function} fn plugin callback
816 * @param {Object} [opts] optional options
817 * @return {Connection} this
818 * @see plugins ./plugins.html
819 * @api public
820 */
821
822Connection.prototype.plugin = function(fn, opts) {
823 this.plugins.push([fn, opts]);
824 return this;
825};
826
827/**
828 * Defines or retrieves a model.
829 *
830 * var mongoose = require('mongoose');
831 * var db = mongoose.createConnection(..);
832 * db.model('Venue', new Schema(..));
833 * var Ticket = db.model('Ticket', new Schema(..));
834 * var Venue = db.model('Venue');
835 *
836 * _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._
837 *
838 * ####Example:
839 *
840 * var schema = new Schema({ name: String }, { collection: 'actor' });
841 *
842 * // or
843 *
844 * schema.set('collection', 'actor');
845 *
846 * // or
847 *
848 * var collectionName = 'actor'
849 * var M = conn.model('Actor', schema, collectionName)
850 *
851 * @param {String|Function} name the model name or class extending Model
852 * @param {Schema} [schema] a schema. necessary when defining a model
853 * @param {String} [collection] name of mongodb collection (optional) if not given it will be induced from model name
854 * @see Mongoose#model #index_Mongoose-model
855 * @return {Model} The compiled model
856 * @api public
857 */
858
859Connection.prototype.model = function(name, schema, collection) {
860 if (!(this instanceof Connection)) {
861 throw new MongooseError('`connection.model()` should not be run with ' +
862 '`new`. If you are doing `new db.model(foo)(bar)`, use ' +
863 '`db.model(foo)(bar)` instead');
864 }
865
866 let fn;
867 if (typeof name === 'function') {
868 fn = name;
869 name = fn.name;
870 }
871
872 // collection name discovery
873 if (typeof schema === 'string') {
874 collection = schema;
875 schema = false;
876 }
877
878 if (utils.isObject(schema) && !schema.instanceOfSchema) {
879 schema = new Schema(schema);
880 }
881 if (schema && !schema.instanceOfSchema) {
882 throw new Error('The 2nd parameter to `mongoose.model()` should be a ' +
883 'schema or a POJO');
884 }
885
886 if (this.models[name] && !collection) {
887 // model exists but we are not subclassing with custom collection
888 if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) {
889 throw new MongooseError.OverwriteModelError(name);
890 }
891 return this.models[name];
892 }
893
894 const opts = {cache: false, connection: this};
895 let model;
896
897 if (schema && schema.instanceOfSchema) {
898 applyPlugins(schema, this.plugins, null, '$connectionPluginsApplied');
899
900 // compile a model
901 model = this.base.model(fn || name, schema, collection, opts);
902
903 // only the first model with this name is cached to allow
904 // for one-offs with custom collection names etc.
905 if (!this.models[name]) {
906 this.models[name] = model;
907 }
908
909 // Errors handled internally, so safe to ignore error
910 model.init(function $modelInitNoop() {});
911
912 return model;
913 }
914
915 if (this.models[name] && collection) {
916 // subclassing current model with alternate collection
917 model = this.models[name];
918 schema = model.prototype.schema;
919 const sub = model.__subclass(this, schema, collection);
920 // do not cache the sub model
921 return sub;
922 }
923
924 // lookup model in mongoose module
925 model = this.base.models[name];
926
927 if (!model) {
928 throw new MongooseError.MissingSchemaError(name);
929 }
930
931 if (this === model.prototype.db
932 && (!collection || collection === model.collection.name)) {
933 // model already uses this connection.
934
935 // only the first model with this name is cached to allow
936 // for one-offs with custom collection names etc.
937 if (!this.models[name]) {
938 this.models[name] = model;
939 }
940
941 return model;
942 }
943 this.models[name] = model.__subclass(this, schema, collection);
944 return this.models[name];
945};
946
947/**
948 * Removes the model named `name` from this connection, if it exists. You can
949 * use this function to clean up any models you created in your tests to
950 * prevent OverwriteModelErrors.
951 *
952 * ####Example:
953 *
954 * conn.model('User', new Schema({ name: String }));
955 * console.log(conn.model('User')); // Model object
956 * conn.deleteModel('User');
957 * console.log(conn.model('User')); // undefined
958 *
959 * // Usually useful in a Mocha `afterEach()` hook
960 * afterEach(function() {
961 * conn.deleteModel(/.+/); // Delete every model
962 * });
963 *
964 * @api public
965 * @param {String|RegExp} name if string, the name of the model to remove. If regexp, removes all models whose name matches the regexp.
966 * @return {Connection} this
967 */
968
969Connection.prototype.deleteModel = function(name) {
970 if (typeof name === 'string') {
971 const model = this.model(name);
972 if (model == null) {
973 return this;
974 }
975 delete this.models[name];
976 delete this.collections[model.collection.name];
977 delete this.base.modelSchemas[name];
978 } else if (name instanceof RegExp) {
979 const pattern = name;
980 const names = this.modelNames();
981 for (const name of names) {
982 if (pattern.test(name)) {
983 this.deleteModel(name);
984 }
985 }
986 } else {
987 throw new Error('First parameter to `deleteModel()` must be a string ' +
988 'or regexp, got "' + name + '"');
989 }
990
991 return this;
992};
993
994/**
995 * Returns an array of model names created on this connection.
996 * @api public
997 * @return {Array}
998 */
999
1000Connection.prototype.modelNames = function() {
1001 return Object.keys(this.models);
1002};
1003
1004/**
1005 * @brief Returns if the connection requires authentication after it is opened. Generally if a
1006 * username and password are both provided than authentication is needed, but in some cases a
1007 * password is not required.
1008 * @api private
1009 * @return {Boolean} true if the connection should be authenticated after it is opened, otherwise false.
1010 */
1011Connection.prototype.shouldAuthenticate = function() {
1012 return this.user != null &&
1013 (this.pass != null || this.authMechanismDoesNotRequirePassword());
1014};
1015
1016/**
1017 * @brief Returns a boolean value that specifies if the current authentication mechanism needs a
1018 * password to authenticate according to the auth objects passed into the openUri methods.
1019 * @api private
1020 * @return {Boolean} true if the authentication mechanism specified in the options object requires
1021 * a password, otherwise false.
1022 */
1023Connection.prototype.authMechanismDoesNotRequirePassword = function() {
1024 if (this.options && this.options.auth) {
1025 return noPasswordAuthMechanisms.indexOf(this.options.auth.authMechanism) >= 0;
1026 }
1027 return true;
1028};
1029
1030/**
1031 * @brief Returns a boolean value that specifies if the provided objects object provides enough
1032 * data to authenticate with. Generally this is true if the username and password are both specified
1033 * but in some authentication methods, a password is not required for authentication so only a username
1034 * is required.
1035 * @param {Object} [options] the options object passed into the openUri methods.
1036 * @api private
1037 * @return {Boolean} true if the provided options object provides enough data to authenticate with,
1038 * otherwise false.
1039 */
1040Connection.prototype.optionsProvideAuthenticationData = function(options) {
1041 return (options) &&
1042 (options.user) &&
1043 ((options.pass) || this.authMechanismDoesNotRequirePassword());
1044};
1045
1046/**
1047 * Switches to a different database using the same connection pool.
1048 *
1049 * Returns a new connection object, with the new db.
1050 *
1051 * @method useDb
1052 * @memberOf Connection
1053 * @param {String} name The database name
1054 * @param {Object} [options]
1055 * @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object.
1056 * @return {Connection} New Connection Object
1057 * @api public
1058 */
1059
1060/*!
1061 * Module exports.
1062 */
1063
1064Connection.STATES = STATES;
1065module.exports = Connection;