diff --git a/History.md b/History.md index d7ce2d8cb54..ee0901b7f77 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,16 @@ +5.12.0 / 2021-03-11 +=================== + * feat(populate): add `transform` option that Mongoose will call on every populated doc #3775 + * feat(query): make `Query#pre()` and `Query#post()` public #9784 + * feat(document): add `Document#getPopulatedDocs()` to return an array of all populated documents in a document #9702 [IslandRhythms](https://github.com/IslandRhythms) + * feat(document): add `Document#getAllSubdocs()` to return an array of all single nested and array subdocuments #9764 [IslandRhythms](https://github.com/IslandRhythms) + * feat(schema): allow `schema` as a schema path name #8798 [IslandRhythms](https://github.com/IslandRhythms) + * feat(QueryCursor): Add batch processing for eachAsync #9902 [khaledosama999](https://github.com/khaledosama999) + * feat(connection): add `noListener` option to help with use cases where you're using `useDb()` on every request #9961 + * feat(index): emit 'createConnection' event when user calls `mongoose.createConnection()` #9985 + * feat(connection+index): emit 'model' and 'deleteModel' events on connections when creating and deleting models #9983 + * feat(query): allow passing `explain` option to `Model.exists()` #8098 [IslandRhythms](https://github.com/IslandRhythms) + 5.11.20 / 2021-03-11 ==================== * fix(query+populate): avoid unnecessarily projecting in subpath when populating a path that uses an elemMatch projection #9973 diff --git a/docs/api/querycursor.html b/docs/api/querycursor.html index 31aeb4dda1d..98f3e5ad8fe 100644 --- a/docs/api/querycursor.html +++ b/docs/api/querycursor.html @@ -20,7 +20,10 @@ «Function»
Returns:

Marks this cursor as closed. Will stop streaming and subsequent calls to next() will error.


QueryCursor.prototype.eachAsync()

Parameters
  • [callback] «Function» executed when all docs have been processed
  • Returns:

    Execute fn for every document in the cursor. If fn returns a promise, will wait for the promise to resolve before iterating on to the next one. Returns a promise that resolves when done.


    QueryCursor.prototype.map()

    Parameters
    Returns:

    Registers a transform function which subsequently maps documents retrieved via the streams interface or .next()

    diff --git a/index.d.ts b/index.d.ts index f9e4066e44f..e43941e5c40 100644 --- a/index.d.ts +++ b/index.d.ts @@ -314,7 +314,7 @@ declare module 'mongoose' { transaction(fn: (session: mongodb.ClientSession) => Promise): Promise; /** Switches to a different database using the same connection pool. */ - useDb(name: string, options?: { useCache?: boolean }): Connection; + useDb(name: string, options?: { useCache?: boolean, noListener?: boolean }): Connection; /** The username specified in the URI */ user: string; @@ -375,6 +375,9 @@ declare module 'mongoose' { /** This documents __v. */ __v?: number; + /* Get all subdocs (by bfs) */ + $getAllSubdocs(): Document[]; + /** Don't run validation on this path or persist changes to this path. */ $ignore(path: string): void; @@ -384,6 +387,9 @@ declare module 'mongoose' { /** Getter/setter, determines whether the document was removed or not. */ $isDeleted(val?: boolean): boolean; + /** Returns an array of all populated documents associated with the query */ + $getPopulatedDocs(): Document[]; + /** * Returns true if the given path is nullish or only contains empty objects. * Useful for determining whether this subdoc will get stripped out by the @@ -2330,12 +2336,13 @@ declare module 'mongoose' { close(callback: (err: CallbackError) => void): void; /** - * Execute `fn` for every document in the cursor. If `fn` returns a promise, + * Execute `fn` for every document(s) in the cursor. If batchSize is provided + * `fn` will be executed for each batch of documents. If `fn` returns a promise, * will wait for the promise to resolve before iterating on to the next one. * Returns a promise that resolves when done. */ - eachAsync(fn: (doc: DocType) => any, options?: { parallel?: number }): Promise; - eachAsync(fn: (doc: DocType) => any, options?: { parallel?: number }, cb?: (err: CallbackError) => void): void; + eachAsync(fn: (doc: DocType| [DocType]) => any, options?: { parallel?: number, batchSize?: number }): Promise; + eachAsync(fn: (doc: DocType| [DocType]) => any, options?: { parallel?: number, batchSize?: number }, cb?: (err: CallbackError) => void): void; /** * Registers a transform function which subsequently maps documents retrieved @@ -2478,9 +2485,6 @@ declare module 'mongoose' { /** Appends new custom $unwind operator(s) to this aggregate pipeline. */ unwind(...args: any[]): this; - - /** Appends new custom $project operator to this aggregate pipeline. */ - project(arg: any): this } class AggregationCursor extends stream.Readable { @@ -2498,12 +2502,13 @@ declare module 'mongoose' { close(callback: (err: CallbackError) => void): void; /** - * Execute `fn` for every document in the cursor. If `fn` returns a promise, + * Execute `fn` for every document(s) in the cursor. If batchSize is provided + * `fn` will be executed for each batch of documents. If `fn` returns a promise, * will wait for the promise to resolve before iterating on to the next one. * Returns a promise that resolves when done. */ - eachAsync(fn: (doc: any) => any, options?: { parallel?: number }): Promise; - eachAsync(fn: (doc: any) => any, options?: { parallel?: number }, cb?: (err: CallbackError) => void): void; + eachAsync(fn: (doc: any) => any, options?: { parallel?: number, batchSize?: number }): Promise; + eachAsync(fn: (doc: any) => any, options?: { parallel?: number, batchSize?: number }, cb?: (err: CallbackError) => void): void; /** * Registers a transform function which subsequently maps documents retrieved diff --git a/lib/connection.js b/lib/connection.js index 5df98fb1954..4ae23003cd0 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -116,11 +116,6 @@ Object.defineProperty(Connection.prototype, 'readyState', { db.readyState = val; } - // loop over relatedDbs on this connection and change their state - for (const k in this.relatedDbs) { - this.relatedDbs[k].readyState = val; - } - if (STATES.connected === val) { this._hasOpened = true; } @@ -1309,6 +1304,8 @@ Connection.prototype.deleteModel = function(name) { delete this.models[name]; delete this.collections[collectionName]; delete this.base.modelSchemas[name]; + + this.emit('deleteModel', model); } else if (name instanceof RegExp) { const pattern = name; const names = this.modelNames(); diff --git a/lib/document.js b/lib/document.js index 9a9c630e342..8229d10d2fe 100644 --- a/lib/document.js +++ b/lib/document.js @@ -78,7 +78,7 @@ function Document(obj, fields, skipId, options) { options.defaults = defaults; // Support `browserDocument.js` syntax - if (this.schema == null) { + if (this.$__schema == null) { const _schema = utils.isObject(fields) && !fields.instanceOfSchema ? new Schema(fields) : fields; @@ -100,7 +100,7 @@ function Document(obj, fields, skipId, options) { throw new ObjectParameterError(obj, 'obj', 'Document'); } - const schema = this.schema; + const schema = this.$__schema; if (typeof fields === 'boolean' || fields === 'throw') { this.$__.strictMode = fields; @@ -203,6 +203,17 @@ for (const i in EventEmitter.prototype) { Document[i] = EventEmitter.prototype[i]; } +/** + * The document's internal schema. + * + * @api private + * @property schema + * @memberOf Document + * @instance + */ + +Document.prototype.$__schema; + /** * The document's schema. * @@ -332,7 +343,7 @@ function $__hasIncludedChildren(fields) { */ function $__applyDefaults(doc, fields, skipId, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) { - const paths = Object.keys(doc.schema.paths); + const paths = Object.keys(doc.$__schema.paths); const plen = paths.length; for (let i = 0; i < plen; ++i) { @@ -344,7 +355,7 @@ function $__applyDefaults(doc, fields, skipId, exclude, hasIncludedChildren, isB continue; } - const type = doc.schema.paths[p]; + const type = doc.$__schema.paths[p]; const path = p.indexOf('.') === -1 ? [p] : p.split('.'); const len = path.length; let included = false; @@ -458,7 +469,7 @@ function $__applyDefaults(doc, fields, skipId, exclude, hasIncludedChildren, isB Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) { const doc = {}; - const paths = Object.keys(this.schema.paths). + const paths = Object.keys(this.$__schema.paths). // Don't build up any paths that are underneath a map, we don't know // what the keys will be filter(p => !p.includes('$*')); @@ -658,12 +669,12 @@ function init(self, obj, doc, opts, prefix) { function _init(index) { i = keys[index]; path = prefix + i; - schema = self.schema.path(path); + schema = self.$__schema.path(path); // Should still work if not a model-level discriminator, but should not be // necessary. This is *only* to catch the case where we queried using the // base model and the discriminated model has a projection - if (self.schema.$isRootDiscriminator && !self.$__isSelected(path)) { + if (self.$__schema.$isRootDiscriminator && !self.$__isSelected(path)) { return; } @@ -768,10 +779,10 @@ Document.prototype.update = function update() { Document.prototype.updateOne = function updateOne(doc, options, callback) { const query = this.constructor.updateOne({ _id: this._id }, doc, options); - query._pre(cb => { + query.pre(cb => { this.constructor._middleware.execPre('updateOne', this, [this], cb); }); - query._post(cb => { + query.post(cb => { this.constructor._middleware.execPost('updateOne', this, [this], {}, cb); }); @@ -841,7 +852,7 @@ Document.prototype.$session = function $session(session) { this.$__.session = session; if (!this.ownerDocument) { - const subdocs = this.$__getAllSubdocs(); + const subdocs = this.$getAllSubdocs(); for (const child of subdocs) { child.$session(session); } @@ -871,10 +882,10 @@ Document.prototype.overwrite = function overwrite(obj) { continue; } // Explicitly skip version key - if (this.schema.options.versionKey && key === this.schema.options.versionKey) { + if (this.$__schema.options.versionKey && key === this.$__schema.options.versionKey) { continue; } - if (this.schema.options.discriminatorKey && key === this.schema.options.discriminatorKey) { + if (this.$__schema.options.discriminatorKey && key === this.$__schema.options.discriminatorKey) { continue; } this.$set(key, obj[key]); @@ -921,7 +932,7 @@ Document.prototype.$set = function $set(path, val, type, options) { if (adhoc) { adhocs = this.$__.adhocPaths || (this.$__.adhocPaths = {}); - adhocs[path] = this.schema.interpretAsType(path, type, this.schema.options); + adhocs[path] = this.$__schema.interpretAsType(path, type, this.$__schema.options); } if (path == null) { @@ -961,7 +972,7 @@ Document.prototype.$set = function $set(path, val, type, options) { for (let i = 0; i < len; ++i) { key = keys[i]; const pathName = prefix + key; - pathtype = this.schema.pathType(pathName); + pathtype = this.$__schema.pathType(pathName); // On initial set, delete any nested keys if we're going to overwrite // them to ensure we keep the user's key order. @@ -984,9 +995,9 @@ Document.prototype.$set = function $set(path, val, type, options) { pathtype !== 'real' && pathtype !== 'adhocOrUndefined' && !(this.$__path(pathName) instanceof MixedSchema) && - !(this.schema.paths[pathName] && - this.schema.paths[pathName].options && - this.schema.paths[pathName].options.ref); + !(this.$__schema.paths[pathName] && + this.$__schema.paths[pathName].options && + this.$__schema.paths[pathName].options.ref); if (someCondition) { this.$__.$setCalled.add(prefix + key); @@ -1005,8 +1016,8 @@ Document.prototype.$set = function $set(path, val, type, options) { if (pathtype === 'real' || pathtype === 'virtual') { // Check for setting single embedded schema to document (gh-3535) let p = path[key]; - if (this.schema.paths[pathName] && - this.schema.paths[pathName].$isSingleNested && + if (this.$__schema.paths[pathName] && + this.$__schema.paths[pathName].$isSingleNested && path[key] instanceof Document) { p = p.toObject({ virtuals: false, transform: false }); } @@ -1031,7 +1042,7 @@ Document.prototype.$set = function $set(path, val, type, options) { this.$__.$setCalled.add(path); } - let pathType = this.schema.pathType(path); + let pathType = this.$__schema.pathType(path); if (pathType === 'adhocOrUndefined') { pathType = getEmbeddedDiscriminatorPath(this, path, { typeOnly: true }); } @@ -1082,8 +1093,8 @@ Document.prototype.$set = function $set(path, val, type, options) { const parts = path.indexOf('.') === -1 ? [path] : path.split('.'); // Might need to change path for top-level alias - if (typeof this.schema.aliases[parts[0]] == 'string') { - parts[0] = this.schema.aliases[parts[0]]; + if (typeof this.$__schema.aliases[parts[0]] == 'string') { + parts[0] = this.$__schema.aliases[parts[0]]; } if (pathType === 'adhocOrUndefined' && strict) { @@ -1094,12 +1105,12 @@ Document.prototype.$set = function $set(path, val, type, options) { const subpath = parts.slice(0, i + 1).join('.'); // If path is underneath a virtual, bypass everything and just set it. - if (i + 1 < parts.length && this.schema.pathType(subpath) === 'virtual') { + if (i + 1 < parts.length && this.$__schema.pathType(subpath) === 'virtual') { mpath.set(path, val, this); return this; } - schema = this.schema.path(subpath); + schema = this.$__schema.path(subpath); if (schema == null) { continue; } @@ -1123,7 +1134,7 @@ Document.prototype.$set = function $set(path, val, type, options) { return this; } } else if (pathType === 'virtual') { - schema = this.schema.virtualpath(path); + schema = this.$__schema.virtualpath(path); schema.applySetters(val, this); return this; } else { @@ -1243,10 +1254,10 @@ Document.prototype.$set = function $set(path, val, type, options) { let popOpts; if (schema.options && - Array.isArray(schema.options[this.schema.options.typeKey]) && - schema.options[this.schema.options.typeKey].length && - schema.options[this.schema.options.typeKey][0].ref && - _isManuallyPopulatedArray(val, schema.options[this.schema.options.typeKey][0].ref)) { + Array.isArray(schema.options[this.$__schema.options.typeKey]) && + schema.options[this.$__schema.options.typeKey].length && + schema.options[this.$__schema.options.typeKey][0].ref && + _isManuallyPopulatedArray(val, schema.options[this.$__schema.options.typeKey][0].ref)) { if (this.ownerDocument) { popOpts = { [populateModelSymbol]: val[0].constructor }; this.ownerDocument().populated(this.$__fullPath(path), @@ -1261,7 +1272,7 @@ Document.prototype.$set = function $set(path, val, type, options) { didPopulate = true; } - if (this.schema.singleNestedPaths[path] == null) { + if (this.$__schema.singleNestedPaths[path] == null) { // If this path is underneath a single nested schema, we'll call the setter // later in `$__set()` because we don't take `_doc` when we iterate through // a single nested doc. That's to make sure we get the correct context. @@ -1420,7 +1431,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, constructing, pa // Re: the note about gh-7196, `val` is the raw value without casting or // setters if the full path is under a single nested subdoc because we don't // want to double run setters. So don't set it as modified. See gh-7264. - if (this.schema.singleNestedPaths[path] != null) { + if (this.$__schema.singleNestedPaths[path] != null) { return false; } @@ -1583,15 +1594,15 @@ Document.prototype.get = function(path, type, options) { let adhoc; options = options || {}; if (type) { - adhoc = this.schema.interpretAsType(path, type, this.schema.options); + adhoc = this.$__schema.interpretAsType(path, type, this.$__schema.options); } let schema = this.$__path(path); if (schema == null) { - schema = this.schema.virtualpath(path); + schema = this.$__schema.virtualpath(path); } if (schema instanceof MixedSchema) { - const virtual = this.schema.virtualpath(path); + const virtual = this.$__schema.virtualpath(path); if (virtual != null) { schema = virtual; } @@ -1604,8 +1615,8 @@ Document.prototype.get = function(path, type, options) { } // Might need to change path for top-level alias - if (typeof this.schema.aliases[pieces[0]] == 'string') { - pieces[0] = this.schema.aliases[pieces[0]]; + if (typeof this.$__schema.aliases[pieces[0]] == 'string') { + pieces[0] = this.$__schema.aliases[pieces[0]]; } for (let i = 0, l = pieces.length; i < l; i++) { @@ -1630,7 +1641,7 @@ Document.prototype.get = function(path, type, options) { if (schema != null && options.getters !== false) { obj = schema.applyGetters(obj, this); - } else if (this.schema.nested[path] && options.virtuals) { + } else if (this.$__schema.nested[path] && options.virtuals) { // Might need to apply virtuals if this is a nested path return applyVirtuals(this, utils.clone(obj) || {}, { path: path }); } @@ -1661,7 +1672,7 @@ Document.prototype.$__path = function(path) { if (adhocType) { return adhocType; } - return this.schema.path(path); + return this.$__schema.path(path); }; /** @@ -2244,7 +2255,7 @@ Document.prototype.validate = function(pathsToValidate, options, callback) { function _evaluateRequiredFunctions(doc) { Object.keys(doc.$__.activePaths.states.require).forEach(path => { - const p = doc.schema.path(path); + const p = doc.$__schema.path(path); if (p != null && typeof p.originalRequiredValue === 'function') { doc.$__.cachedRequired[path] = p.originalRequiredValue.call(doc, doc); @@ -2277,7 +2288,7 @@ function _getPathsToValidate(doc) { Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths); function addToPaths(p) { paths.add(p); } - const subdocs = doc.$__getAllSubdocs(); + const subdocs = doc.$getAllSubdocs(); const modifiedPaths = doc.modifiedPaths(); for (const subdoc of subdocs) { if (subdoc.$basePath) { @@ -2304,7 +2315,7 @@ function _getPathsToValidate(doc) { // gh-661: if a whole array is modified, make sure to run validation on all // the children as well for (const path of paths) { - const _pathType = doc.schema.path(path); + const _pathType = doc.$__schema.path(path); if (!_pathType || !_pathType.$isMongooseArray || // To avoid potential performance issues, skip doc arrays whose children @@ -2333,12 +2344,12 @@ function _getPathsToValidate(doc) { const flattenOptions = { skipArrays: true }; for (const pathToCheck of paths) { - if (doc.schema.nested[pathToCheck]) { + if (doc.$__schema.nested[pathToCheck]) { let _v = doc.$__getValue(pathToCheck); if (isMongooseObject(_v)) { _v = _v.toObject({ transform: false }); } - const flat = flatten(_v, pathToCheck, flattenOptions, doc.schema); + const flat = flatten(_v, pathToCheck, flattenOptions, doc.$__schema); Object.keys(flat).forEach(addToPaths); } } @@ -2347,11 +2358,11 @@ function _getPathsToValidate(doc) { // Single nested paths (paths embedded under single nested subdocs) will // be validated on their own when we call `validate()` on the subdoc itself. // Re: gh-8468 - if (doc.schema.singleNestedPaths.hasOwnProperty(path)) { + if (doc.$__schema.singleNestedPaths.hasOwnProperty(path)) { paths.delete(path); continue; } - const _pathType = doc.schema.path(path); + const _pathType = doc.$__schema.path(path); if (!_pathType || !_pathType.$isSchemaMap) { continue; } @@ -2391,7 +2402,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { if (hasValidateModifiedOnlyOption) { shouldValidateModifiedOnly = !!options.validateModifiedOnly; } else { - shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly; + shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly; } const _this = this; @@ -2443,7 +2454,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { return process.nextTick(function() { const error = _complete(); if (error) { - return _this.schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { callback(error); }); } @@ -2457,7 +2468,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { const complete = function() { const error = _complete(); if (error) { - return _this.schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('validate:error', _this, [_this], { error: error }, function(error) { callback(error); }); } @@ -2473,7 +2484,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { total++; process.nextTick(function() { - const schemaType = _this.schema.path(path); + const schemaType = _this.$__schema.path(path); if (!schemaType) { return --total || complete(); @@ -2590,7 +2601,7 @@ Document.prototype.validateSync = function(pathsToValidate, options) { if (hasValidateModifiedOnlyOption) { shouldValidateModifiedOnly = !!options.validateModifiedOnly; } else { - shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly; + shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly; } if (typeof pathsToValidate === 'string') { @@ -2616,7 +2627,7 @@ Document.prototype.validateSync = function(pathsToValidate, options) { validating[path] = true; - const p = _this.schema.path(path); + const p = _this.$__schema.path(path); if (!p) { return; } @@ -2916,7 +2927,7 @@ Document.prototype.$__reset = function reset() { this.$__.validationError = undefined; this.errors = undefined; _this = this; - this.schema.requiredPaths().forEach(function(path) { + this.$__schema.requiredPaths().forEach(function(path) { _this.$__.activePaths.require(path); }); @@ -2946,7 +2957,7 @@ Document.prototype.$__undoReset = function $__undoReset() { } } - for (const subdoc of this.$__getAllSubdocs()) { + for (const subdoc of this.$getAllSubdocs()) { subdoc.$__undoReset(); } }; @@ -3037,8 +3048,10 @@ Document.prototype.$__setSchema = function(schema) { for (const key of Object.keys(schema.virtuals)) { schema.virtuals[key]._applyDefaultGetters(); } - - this.schema = schema; + if (schema.path('schema') == null) { + this.schema = schema; + } + this.$__schema = schema; this[documentSchemaSymbol] = schema; }; @@ -3074,13 +3087,13 @@ Document.prototype.$__getArrayPathsToValidate = function() { /** * Get all subdocs (by bfs) * - * @api private - * @method $__getAllSubdocs + * @api public + * @method $getAllSubdocs * @memberOf Document * @instance */ -Document.prototype.$__getAllSubdocs = function() { +Document.prototype.$getAllSubdocs = function $getAllSubdocs() { DocumentArray || (DocumentArray = require('./types/documentarray')); Embedded = Embedded || require('./types/embedded'); @@ -3137,7 +3150,7 @@ Document.prototype.$__getAllSubdocs = function() { */ function applyQueue(doc) { - const q = doc.schema && doc.schema.callQueue; + const q = doc.$__schema && doc.$__schema.callQueue; if (!q.length) { return; } @@ -3179,7 +3192,7 @@ Document.prototype.$toObject = function(options, json) { const path = json ? 'toJSON' : 'toObject'; const baseOptions = get(this, 'constructor.base.options.' + path, {}); - const schemaOptions = get(this, 'schema.options', {}); + const schemaOptions = get(this, '$__schema.options', {}); // merge base default options with Schema's set default options if available. // `clone` is necessary here because `utils.options` directly modifies the second input. defaultOptions = utils.options(defaultOptions, clone(baseOptions)); @@ -3258,8 +3271,8 @@ Document.prototype.$toObject = function(options, json) { applyVirtuals(this, ret, gettersOptions, options); } - if (options.versionKey === false && this.schema.options.versionKey) { - delete ret[this.schema.options.versionKey]; + if (options.versionKey === false && this.$__schema.options.versionKey) { + delete ret[this.$__schema.options.versionKey]; } let transform = options.transform; @@ -3489,7 +3502,7 @@ function minimize(obj) { */ function applyVirtuals(self, json, options, toObjectOptions) { - const schema = self.schema; + const schema = self.$__schema; const paths = Object.keys(schema.virtuals); let i = paths.length; const numPaths = i; @@ -3548,7 +3561,7 @@ function applyVirtuals(self, json, options, toObjectOptions) { */ function applyGetters(self, json, options) { - const schema = self.schema; + const schema = self.$__schema; const paths = Object.keys(schema.paths); let i = paths.length; let path; @@ -3603,7 +3616,7 @@ function applyGetters(self, json, options) { */ function applySchemaTypeTransforms(self, json) { - const schema = self.schema; + const schema = self.$__schema; const paths = Object.keys(schema.paths || {}); const cur = self._doc; @@ -3646,7 +3659,7 @@ function throwErrorIfPromise(path, transformedValue) { */ function omitDeselectedFields(self, json) { - const schema = self.schema; + const schema = self.$__schema; const paths = Object.keys(schema.paths || {}); const cur = self._doc; @@ -3901,6 +3914,21 @@ Document.prototype.populate = function populate() { return this; }; +/* Returns an array of all populated documents associated with the query. */ +Document.prototype.$getPopulatedDocs = function $getPopulatedDocs() { + const keys = (Object.keys(this.$__.populated)); + let result = []; + for (const key of keys) { + const value = this.get(key); + if (Array.isArray(value)) { + result = result.concat(value); + } else if (value instanceof Document) { + result.push(value); + } + } + return result; +}; + /** * Explicitly executes population and returns a promise. Useful for promises integration. * diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 57e7195448a..e032eec1960 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -38,16 +38,20 @@ NativeConnection.prototype.__proto__ = MongooseConnection.prototype; * Returns a new connection object, with the new db. If you set the `useCache` * option, `useDb()` will cache connections by `name`. * + * **Note:** Calling `close()` on a `useDb()` connection will close the base connection as well. + * * @param {String} name The database name * @param {Object} [options] * @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object. + * @param {Boolean} [options.noListener=false] If true, the new connection object won't listen to any events on the base connection. This is better for memory usage in cases where you're calling `useDb()` for every request. * @return {Connection} New Connection Object * @api public */ NativeConnection.prototype.useDb = function(name, options) { // Return immediately if cached - if (options && options.useCache && this.relatedDbs[name]) { + options = options || {}; + if (options.useCache && this.relatedDbs[name]) { return this.relatedDbs[name]; } @@ -90,16 +94,24 @@ NativeConnection.prototype.useDb = function(name, options) { function wireup() { newConn.client = _this.client; - newConn.db = _this.client.db(name); + const _opts = {}; + if (options.hasOwnProperty('noListener')) { + _opts.noListener = options.noListener; + } + newConn.db = _this.client.db(name, _opts); newConn.onOpen(); // setup the events appropriately - listen(newConn); + if (options.noListener !== true) { + listen(newConn); + } } newConn.name = name; // push onto the otherDbs stack, this is used when state changes - this.otherDbs.push(newConn); + if (options.noListener !== true) { + this.otherDbs.push(newConn); + } newConn.otherDbs.push(this); // push onto the relatedDbs cache, this is used when state changes diff --git a/lib/helpers/cursor/eachAsync.js b/lib/helpers/cursor/eachAsync.js index 4afff032fe8..96e9c933a75 100644 --- a/lib/helpers/cursor/eachAsync.js +++ b/lib/helpers/cursor/eachAsync.js @@ -22,6 +22,7 @@ const promiseOrCallback = require('../promiseOrCallback'); module.exports = function eachAsync(next, fn, options, callback) { const parallel = options.parallel || 1; + const batchSize = options.batchSize; const enqueue = asyncQueue(); return promiseOrCallback(callback, cb => { @@ -32,6 +33,7 @@ module.exports = function eachAsync(next, fn, options, callback) { let drained = false; let handleResultsInProgress = 0; let currentDocumentIndex = 0; + let documentsBatch = []; let error = null; for (let i = 0; i < parallel; ++i) { @@ -57,6 +59,8 @@ module.exports = function eachAsync(next, fn, options, callback) { if (handleResultsInProgress <= 0) { finalCallback(null); } + else if (batchSize && documentsBatch.length) + handleNextResult(documentsBatch, currentDocumentIndex++, handleNextResultCallBack); return done(); } @@ -66,8 +70,25 @@ module.exports = function eachAsync(next, fn, options, callback) { // make sure we know that we still have a result to handle re: #8422 process.nextTick(() => done()); - handleNextResult(doc, currentDocumentIndex++, function(err) { - --handleResultsInProgress; + if (batchSize) { + documentsBatch.push(doc); + } + + // If the current documents size is less than the provided patch size don't process the documents yet + if (batchSize && documentsBatch.length !== batchSize) { + setTimeout(() => enqueue(fetch), 0); + return; + } + + const docsToProcess = batchSize ? documentsBatch : doc; + + function handleNextResultCallBack(err) { + if (batchSize) { + handleResultsInProgress -= documentsBatch.length; + documentsBatch = []; + } + else + --handleResultsInProgress; if (err != null) { error = err; return finalCallback(err); @@ -77,7 +98,9 @@ module.exports = function eachAsync(next, fn, options, callback) { } setTimeout(() => enqueue(fetch), 0); - }); + } + + handleNextResult(docsToProcess, currentDocumentIndex++, handleNextResultCallBack); }); } } diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js index 252d34824df..98de475364f 100644 --- a/lib/helpers/document/cleanModifiedSubpaths.js +++ b/lib/helpers/document/cleanModifiedSubpaths.js @@ -14,7 +14,7 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { } for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) { if (skipDocArrays) { - const schemaType = doc.schema.path(modifiedPath); + const schemaType = doc.$__schema.path(modifiedPath); if (schemaType && schemaType.$isMongooseDocumentArray) { continue; } diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js index def45e67b23..47e15f4d6cc 100644 --- a/lib/helpers/document/compile.js +++ b/lib/helpers/document/compile.js @@ -74,6 +74,13 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { value: prototype.schema }); + Object.defineProperty(nested, '$__schema', { + enumerable: false, + configurable: true, + writable: false, + value: prototype.schema + }); + Object.defineProperty(nested, documentSchemaSymbol, { enumerable: false, configurable: true, diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index a09b1007473..2cbe3939cc6 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -12,7 +12,7 @@ const utils = require('../../utils'); module.exports = function assignVals(o) { // Options that aren't explicitly listed in `populateOptions` - const userOptions = get(o, 'allOptions.options.options'); + const userOptions = Object.assign({}, get(o, 'allOptions.options.options'), get(o, 'allOptions.options')); // `o.options` contains options explicitly listed in `populateOptions`, like // `match` and `limit`. const populateOptions = Object.assign({}, o.options, userOptions, { @@ -25,6 +25,7 @@ module.exports = function assignVals(o) { // replace the original ids in our intermediate _ids structure // with the documents found by query + o.allIds = [].concat(o.allIds); assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions); // now update the original documents being populated using the @@ -34,6 +35,8 @@ module.exports = function assignVals(o) { const options = o.options; const count = o.count && o.isVirtual; + let i; + function setValue(val) { if (count) { return val; @@ -42,6 +45,8 @@ module.exports = function assignVals(o) { return val.val; } + const _allIds = o.allIds[i]; + if (o.justOne === true && Array.isArray(val)) { // Might be an embedded discriminator (re: gh-9244) with multiple models, so make sure to pick the right // model before assigning. @@ -61,14 +66,14 @@ module.exports = function assignVals(o) { val[i] = ret[i]; } - return valueFilter(val[0], options, populateOptions, populatedModel); + return valueFilter(val[0], options, populateOptions, _allIds); } else if (o.justOne === false && !Array.isArray(val)) { - return valueFilter([val], options, populateOptions, populatedModel); + return valueFilter([val], options, populateOptions, _allIds); } - return valueFilter(val, options, populateOptions, populatedModel); + return valueFilter(val, options, populateOptions, _allIds); } - for (let i = 0; i < docs.length; ++i) { + for (i = 0; i < docs.length; ++i) { const existingVal = mpath.get(o.path, docs[i], lookupLocalFields); if (existingVal == null && !getVirtual(o.originalModel.schema, o.path)) { continue; @@ -195,15 +200,20 @@ function numDocs(v) { * that population mapping can occur. */ -function valueFilter(val, assignmentOpts, populateOptions) { +function valueFilter(val, assignmentOpts, populateOptions, allIds) { + const userSpecifiedTransform = typeof populateOptions.transform === 'function'; + const transform = userSpecifiedTransform ? populateOptions.transform : noop; if (Array.isArray(val)) { // find logic const ret = []; const numValues = val.length; for (let i = 0; i < numValues; ++i) { - const subdoc = val[i]; - if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null)) { + let subdoc = val[i]; + const _allIds = Array.isArray(allIds) ? allIds[i] : allIds; + if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null) && !userSpecifiedTransform) { continue; + } else if (userSpecifiedTransform) { + subdoc = transform(isPopulatedObject(subdoc) ? subdoc : null, _allIds); } maybeRemoveId(subdoc, assignmentOpts); ret.push(subdoc); @@ -227,7 +237,7 @@ function valueFilter(val, assignmentOpts, populateOptions) { // findOne if (isPopulatedObject(val)) { maybeRemoveId(val, assignmentOpts); - return val; + return transform(val, allIds); } if (val instanceof Map) { @@ -235,12 +245,12 @@ function valueFilter(val, assignmentOpts, populateOptions) { } if (populateOptions.justOne === true) { - return (val == null ? val : null); + return val == null ? transform(val, allIds) : transform(null, allIds); } if (populateOptions.justOne === false) { return []; } - return val == null ? val : null; + return val == null ? transform(val, allIds) : transform(null, allIds); } /*! @@ -271,4 +281,8 @@ function isPopulatedObject(obj) { obj.$isMongooseMap || obj.$__ != null || leanPopulateMap.has(obj); +} + +function noop(v) { + return v; } \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 1425cfac69a..386b4d2a60e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -16,6 +16,7 @@ if (global.MONGOOSE_DRIVER_PATH) { } const Document = require('./document'); +const EventEmitter = require('events').EventEmitter; const Schema = require('./schema'); const SchemaType = require('./schematype'); const SchemaTypes = require('./schema/index'); @@ -65,6 +66,7 @@ function Mongoose(options) { this.connections = []; this.models = {}; this.modelSchemas = {}; + this.events = new EventEmitter(); // default global options this.options = Object.assign({ pluralization: true @@ -280,6 +282,7 @@ Mongoose.prototype.createConnection = function(uri, options, callback) { options = null; } _mongoose.connections.push(conn); + _mongoose.events.emit('createConnection', conn); if (arguments.length > 0) { return conn.openUri(uri, options, callback); @@ -581,6 +584,8 @@ Mongoose.prototype.model = function(name, schema, collection, skipInit) { model.init(function $modelInitNoop() {}); } + connection.emit('model', model); + if (options.cache === false) { return model; } diff --git a/lib/model.js b/lib/model.js index 82fd28c27cf..2b5fbb3b5f2 100644 --- a/lib/model.js +++ b/lib/model.js @@ -231,7 +231,7 @@ Model.prototype.$__handleSave = function(options, callback) { if ('safe' in options) { _handleSafe(options); } - applyWriteConcern(this.schema, options); + applyWriteConcern(this.$__schema, options); if ('w' in options) { saveOptions.w = options.w; } @@ -346,7 +346,7 @@ Model.prototype.$__handleSave = function(options, callback) { Model.prototype.$__save = function(options, callback) { this.$__handleSave(options, (error, result) => { - const hooks = this.schema.s.hooks; + const hooks = this.$__schema.s.hooks; if (error) { return hooks.execPost('save:error', this, [this], { error: error }, (error) => { callback(error, this); @@ -374,7 +374,7 @@ Model.prototype.$__save = function(options, callback) { const doIncrement = VERSION_INC === (VERSION_INC & this.$__.version); this.$__.version = undefined; - const key = this.schema.options.versionKey; + const key = this.$__schema.options.versionKey; const version = this.$__getValue(key) || 0; if (numAffected <= 0) { @@ -413,7 +413,7 @@ Model.prototype.$__save = function(options, callback) { */ function generateVersionError(doc, modifiedPaths) { - const key = doc.schema.options.versionKey; + const key = doc.$__schema.options.versionKey; if (!key) { return null; } @@ -512,7 +512,7 @@ Model.prototype.save = function(options, fn) { * @return {Boolean} true if versioning should be skipped for the given path */ function shouldSkipVersioning(self, path) { - const skipVersioning = self.schema.options.skipVersioning; + const skipVersioning = self.$__schema.options.skipVersioning; if (!skipVersioning) return false; // Remove any array indexes from the path @@ -539,7 +539,7 @@ function operand(self, where, delta, data, val, op) { if (!delta[op]) delta[op] = {}; delta[op][data.path] = val; // disabled versioning? - if (self.schema.options.versionKey === false) return; + if (self.$__schema.options.versionKey === false) return; // path excluded from versioning? if (shouldSkipVersioning(self, data.path)) return; @@ -547,7 +547,7 @@ function operand(self, where, delta, data, val, op) { // already marked for versioning? if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return; - if (self.schema.options.optimisticConcurrency) { + if (self.$__schema.options.optimisticConcurrency) { self.$__.version = VERSION_ALL; return; } @@ -809,7 +809,7 @@ function checkDivergentArray(doc, path, array) { */ Model.prototype.$__version = function(where, delta) { - const key = this.schema.options.versionKey; + const key = this.$__schema.options.versionKey; if (where === true) { // this is an insert @@ -1057,7 +1057,6 @@ Model.prototype.model = function model(name) { Model.exists = function exists(filter, options, callback) { _checkContext(this, 'exists'); - if (typeof options === 'function') { callback = options; options = null; @@ -1077,8 +1076,12 @@ Model.exists = function exists(filter, options, callback) { }); return; } + options = options || {}; + if (!options.explain) { + return query.then(doc => !!doc); + } - return query.then(doc => !!doc); + return query.exec(); }; /** @@ -3315,8 +3318,8 @@ Model.$__insertMany = function(arr, options, callback) { return; } const docObjects = docAttributes.map(function(doc) { - if (doc.schema.options.versionKey) { - doc[doc.schema.options.versionKey] = 0; + if (doc.$__schema.options.versionKey) { + doc[doc.$__schema.options.versionKey] = 0; } if (doc.initializeTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); @@ -3388,7 +3391,7 @@ function _setIsNew(doc, val) { doc.emit('isNew', val); doc.constructor.emit('isNew', val); - const subdocs = doc.$__getAllSubdocs(); + const subdocs = doc.$getAllSubdocs(); for (const subdoc of subdocs) { subdoc.isNew = val; } @@ -4265,6 +4268,7 @@ Model.geoSearch = function(conditions, options, callback) { * @param {Boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type. * @param {Number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results because it only executes a single query for every document being populated. If you set `perDocumentLimit`, Mongoose will ensure correct `limit` per document by executing a separate query for each document to `populate()`. For example, `.find().populate({ path: 'test', perDocumentLimit: 2 })` will execute 2 additional queries if `.find()` returns 2 documents. * @param {Object} [options.options=null] Additional options like `limit` and `lean`. + * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document. * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Promise} * @api public @@ -4763,7 +4767,7 @@ Model.compile = function compile(name, schema, collectionName, connection, base) applyHooks(model, schema); applyStaticHooks(model, schema.s.hooks, schema.statics); - model.schema = model.prototype.schema; + model.schema = model.prototype.$__schema; model.collection = model.prototype.collection; // Create custom query constructor @@ -4829,13 +4833,13 @@ Model.__subclass = function subclass(conn, schema, collection) { const s = schema && typeof schema !== 'string' ? schema - : _this.prototype.schema; + : _this.prototype.$__schema; const options = s.options || {}; const _userProvidedOptions = s._userProvidedOptions || {}; if (!collection) { - collection = _this.prototype.schema.get('collection') || + collection = _this.prototype.$__schema.get('collection') || utils.toCollectionName(_this.modelName, this.base.pluralize()); } diff --git a/lib/plugins/removeSubdocs.js b/lib/plugins/removeSubdocs.js index 44b2ea62790..c2756fc5374 100644 --- a/lib/plugins/removeSubdocs.js +++ b/lib/plugins/removeSubdocs.js @@ -15,13 +15,13 @@ module.exports = function(schema) { } const _this = this; - const subdocs = this.$__getAllSubdocs(); + const subdocs = this.$getAllSubdocs(); each(subdocs, function(subdoc, cb) { subdoc.$__remove(cb); }, function(error) { if (error) { - return _this.schema.s.hooks.execPost('remove:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('remove:error', _this, [_this], { error: error }, function(error) { next(error); }); } diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js index c0a3144e778..fcc73d88a71 100644 --- a/lib/plugins/saveSubdocs.js +++ b/lib/plugins/saveSubdocs.js @@ -15,7 +15,7 @@ module.exports = function(schema) { } const _this = this; - const subdocs = this.$__getAllSubdocs(); + const subdocs = this.$getAllSubdocs(); if (!subdocs.length) { next(); @@ -23,12 +23,12 @@ module.exports = function(schema) { } each(subdocs, function(subdoc, cb) { - subdoc.schema.s.hooks.execPre('save', subdoc, function(err) { + subdoc.$__schema.s.hooks.execPre('save', subdoc, function(err) { cb(err); }); }, function(error) { if (error) { - return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { next(error); }); } @@ -43,7 +43,7 @@ module.exports = function(schema) { } const _this = this; - const subdocs = this.$__getAllSubdocs(); + const subdocs = this.$getAllSubdocs(); if (!subdocs.length) { next(); @@ -51,12 +51,12 @@ module.exports = function(schema) { } each(subdocs, function(subdoc, cb) { - subdoc.schema.s.hooks.execPost('save', subdoc, [subdoc], function(err) { + subdoc.$__schema.s.hooks.execPost('save', subdoc, [subdoc], function(err) { cb(err); }); }, function(error) { if (error) { - return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { next(error); }); } diff --git a/lib/plugins/sharding.js b/lib/plugins/sharding.js index 560053ed30c..020ec06c633 100644 --- a/lib/plugins/sharding.js +++ b/lib/plugins/sharding.js @@ -56,7 +56,7 @@ module.exports.storeShard = storeShard; function storeShard() { // backwards compat - const key = this.schema.options.shardKey || this.schema.options.shardkey; + const key = this.$__schema.options.shardKey || this.$__schema.options.shardkey; if (!utils.isPOJO(key)) { return; } diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index 410a596f3bc..30ded8785f2 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -18,8 +18,8 @@ module.exports = function trackTransaction(schema) { if (this.isNew) { initialState.isNew = true; } - if (this.schema.options.versionKey) { - initialState.versionKey = this.get(this.schema.options.versionKey); + if (this.$__schema.options.versionKey) { + initialState.versionKey = this.get(this.$__schema.options.versionKey); } initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.states.modify)); diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js index 4635de1ccfb..c06d5e6e2c4 100644 --- a/lib/plugins/validateBeforeSave.js +++ b/lib/plugins/validateBeforeSave.js @@ -21,7 +21,7 @@ module.exports = function(schema) { if (hasValidateBeforeSaveOption) { shouldValidate = !!options.validateBeforeSave; } else { - shouldValidate = this.schema.options.validateBeforeSave; + shouldValidate = this.$__schema.options.validateBeforeSave; } // Validate @@ -33,7 +33,7 @@ module.exports = function(schema) { { validateModifiedOnly: options.validateModifiedOnly } : null; this.validate(validateOptions, function(error) { - return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { _this.$op = 'save'; next(error); }); diff --git a/lib/query.js b/lib/query.js index 5b914c31faa..ba43a98c912 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1339,7 +1339,6 @@ Query.prototype.setOptions = function(options, overwrite) { } return this; } - if (options == null) { return this; } @@ -4472,20 +4471,54 @@ Query.prototype.catch = function(reject) { return this.exec().then(null, reject); }; -/*! - * ignore +/** + * Add pre [middleware](/docs/middleware.html) to this query instance. Doesn't affect + * other queries. + * + * ####Example: + * + * const q1 = Question.find({ answer: 42 }); + * q1.pre(function middleware() { + * console.log(this.getFilter()); + * }); + * await q1.exec(); // Prints "{ answer: 42 }" + * + * // Doesn't print anything, because `middleware()` is only + * // registered on `q1`. + * await Question.find({ answer: 42 }); + * + * @param {Function} fn + * @return {Promise} + * @api public */ -Query.prototype._pre = function(fn) { +Query.prototype.pre = function(fn) { this._hooks.pre('exec', fn); return this; }; -/*! - * ignore +/** + * Add post [middleware](/docs/middleware.html) to this query instance. Doesn't affect + * other queries. + * + * ####Example: + * + * const q1 = Question.find({ answer: 42 }); + * q1.post(function middleware() { + * console.log(this.getFilter()); + * }); + * await q1.exec(); // Prints "{ answer: 42 }" + * + * // Doesn't print anything, because `middleware()` is only + * // registered on `q1`. + * await Question.find({ answer: 42 }); + * + * @param {Function} fn + * @return {Promise} + * @api public */ -Query.prototype._post = function(fn) { +Query.prototype.post = function(fn) { this._hooks.post('exec', fn); return this; }; @@ -4626,6 +4659,7 @@ function castDoc(query, overwrite) { * @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](/docs/schematypes.html#schematype-options). * @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them. * @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://docs.mongodb.com/manual/tutorial/query-documents/), or a function that returns a filter object. + * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document. * @param {Object} [options.options=null] Additional options like `limit` and `lean`. * @see population ./populate.html * @see Query#select #query_Query-select @@ -4756,7 +4790,6 @@ Query.prototype.cast = function(model, obj) { obj || (obj = this._conditions); model = model || this.model; - const discriminatorKey = model.schema.options.discriminatorKey; if (obj != null && obj.hasOwnProperty(discriminatorKey)) { diff --git a/lib/schema.js b/lib/schema.js index feb978ccb87..fa4b2fda528 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -591,7 +591,6 @@ reserved.isNew = reserved.populated = reserved.remove = reserved.save = -reserved.schema = reserved.toObject = reserved.validate = 1; diff --git a/lib/types/core_array.js b/lib/types/core_array.js index eb71de05e67..cdad35c686c 100644 --- a/lib/types/core_array.js +++ b/lib/types/core_array.js @@ -248,8 +248,8 @@ class CoreMongooseArray extends Array { // gh-2399 // we should cast model only when it's not a discriminator - const isDisc = value.schema && value.schema.discriminatorMapping && - value.schema.discriminatorMapping.key !== undefined; + const isDisc = value.$__schema && value.$__schema.discriminatorMapping && + value.$__schema.discriminatorMapping.key !== undefined; if (!isDisc) { value = new Model(value); } diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 182fa229f13..5a8612b147a 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -25,9 +25,9 @@ function Subdocument(value, fields, parent, skipId, options) { let initedPaths = null; if (hasPriorDoc) { this._doc = Object.assign({}, options.priorDoc._doc); - delete this._doc[this.schema.options.discriminatorKey]; + delete this._doc[this.$__schema.options.discriminatorKey]; initedPaths = Object.keys(options.priorDoc._doc || {}). - filter(key => key !== this.schema.options.discriminatorKey); + filter(key => key !== this.$__schema.options.discriminatorKey); } if (parent != null) { // If setting a nested path, should copy isNew from parent re: gh-7048 @@ -43,7 +43,7 @@ function Subdocument(value, fields, parent, skipId, options) { if (!this.$__.activePaths.states.modify[key] && !this.$__.activePaths.states.default[key] && !this.$__.$setCalled.has(key)) { - const schematype = this.schema.path(key); + const schematype = this.$__schema.path(key); const def = schematype == null ? void 0 : schematype.getDefault(this); if (def === void 0) { delete this._doc[key]; diff --git a/package.json b/package.json index 064840596bc..52801b2e9ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "5.11.20", + "version": "5.12.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", diff --git a/test/connection.test.js b/test/connection.test.js index 74fa858d4f6..3eab9f19ce9 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1124,16 +1124,22 @@ describe('connections:', function() { it('deleteModel()', function() { const conn = mongoose.createConnection('mongodb://localhost:27017/gh6813'); - conn.model('gh6813', new Schema({ name: String })); + let Model = conn.model('gh6813', new Schema({ name: String })); + + const events = []; + conn.on('deleteModel', model => events.push(model)); assert.ok(conn.model('gh6813')); conn.deleteModel('gh6813'); + assert.equal(events.length, 1); + assert.equal(events[0], Model); + assert.throws(function() { conn.model('gh6813'); }, /Schema hasn't been registered/); - const Model = conn.model('gh6813', new Schema({ name: String })); + Model = conn.model('gh6813', new Schema({ name: String })); assert.ok(Model); return Model.create({ name: 'test' }); }); @@ -1242,9 +1248,20 @@ describe('connections:', function() { it('allows overwriting models (gh-9406)', function() { const m = new mongoose.Mongoose(); + const events = []; + m.connection.on('model', model => events.push(model)); + const M1 = m.model('Test', Schema({ name: String }), null, { overwriteModels: true }); + assert.equal(events.length, 1); + assert.equal(events[0], M1); + const M2 = m.model('Test', Schema({ name: String }), null, { overwriteModels: true }); + assert.equal(events.length, 2); + assert.equal(events[1], M2); + const M3 = m.connection.model('Test', Schema({ name: String }), null, { overwriteModels: true }); + assert.equal(events.length, 3); + assert.equal(events[2], M3); assert.ok(M1 !== M2); assert.ok(M2 !== M3); diff --git a/test/document.test.js b/test/document.test.js index 7f0a225531b..b00a643f4c3 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -9885,6 +9885,32 @@ describe('document', function() { }); }); + it('supports getting a list of populated docs (gh-9702)', function() { + const Child = db.model('Child', Schema({ name: String })); + const Parent = db.model('Parent', { + children: [{ type: ObjectId, ref: 'Child' }], + child: { type: ObjectId, ref: 'Child' } + }); + + return co(function*() { + const c = yield Child.create({ name: 'test' }); + yield Parent.create({ + children: [c._id], + child: c._id + }); + + const p = yield Parent.findOne().populate('children child'); + + p.children; // [{ _id: '...', name: 'test' }] + + assert.equal(p.$getPopulatedDocs().length, 2); + assert.equal(p.$getPopulatedDocs()[0], p.children[0]); + assert.equal(p.$getPopulatedDocs()[0].name, 'test'); + assert.equal(p.$getPopulatedDocs()[1], p.child); + assert.equal(p.$getPopulatedDocs()[1].name, 'test'); + }); + }); + it('handles paths named `db` (gh-9798)', function() { const schema = new Schema({ db: String @@ -9902,6 +9928,60 @@ describe('document', function() { }); }); + it('handles paths named `schema` gh-8798', function() { + const schema = new Schema({ + schema: String, + name: String + }); + const Test = db.model('Test', schema); + + return co(function*() { + const doc = yield Test.create({ schema: 'test', name: 'test' }); + yield doc.save(); + assert.ok(doc); + assert.equal(doc.schema, 'test'); + assert.equal(doc.name, 'test'); + + const fromDb = yield Test.findById(doc); + assert.equal(fromDb.schema, 'test'); + assert.equal(fromDb.name, 'test'); + + doc.schema = 'test2'; + yield doc.save(); + + yield fromDb.remove(); + doc.name = 'test3'; + const err = yield doc.save().then(() => null, err => err); + assert.ok(err); + assert.equal(err.name, 'DocumentNotFoundError'); + }); + }); + + it('handles nested paths named `schema` gh-8798', function() { + const schema = new Schema({ + nested: { + schema: String + }, + name: String + }); + const Test = db.model('Test', schema); + + return co(function*() { + const doc = yield Test.create({ nested: { schema: 'test' }, name: 'test' }); + yield doc.save(); + assert.ok(doc); + assert.equal(doc.nested.schema, 'test'); + assert.equal(doc.name, 'test'); + + const fromDb = yield Test.findById(doc); + assert.equal(fromDb.nested.schema, 'test'); + assert.equal(fromDb.name, 'test'); + + doc.nested.schema = 'test2'; + yield doc.save(); + }); + }); + it('object setters will be applied for each object in array after populate (gh-9838)', function() { const updatedElID = '123456789012345678901234'; diff --git a/test/document.unit.test.js b/test/document.unit.test.js index bc7fbd3bc50..0ebdc16dc1b 100644 --- a/test/document.unit.test.js +++ b/test/document.unit.test.js @@ -19,7 +19,7 @@ describe('sharding', function() { } }; const Stub = function() { - this.schema = mockSchema; + this.$__schema = mockSchema; this.$__ = {}; }; Stub.prototype.__proto__ = mongoose.Document.prototype; @@ -37,7 +37,7 @@ describe('toObject()', function() { beforeEach(function() { Stub = function() { - const schema = this.schema = { + const schema = this.$__schema = { options: { toObject: { minimize: false, virtuals: true } }, virtuals: { virtual: 'test' } }; diff --git a/test/helpers/cursor.eachAsync.test.js b/test/helpers/cursor.eachAsync.test.js index b7d847b055a..5ea083ef3e5 100644 --- a/test/helpers/cursor.eachAsync.test.js +++ b/test/helpers/cursor.eachAsync.test.js @@ -59,4 +59,79 @@ describe('eachAsync()', function() { then(() => eachAsync(next, fn, { parallel: 2 })). then(() => assert.equal(numDone, max)); }); + + it('it processes the documents in batches successfully', () => { + const batchSize = 3; + let numberOfDocuments = 0; + const maxNumberOfDocuments = 9; + let numberOfBatchesProcessed = 0; + + function next(cb) { + setTimeout(() => { + if (++numberOfDocuments > maxNumberOfDocuments) { + cb(null, null); + } + return cb(null, { id: numberOfDocuments }); + }, 0); + } + + const fn = (docs, index) => { + assert.equal(docs.length, batchSize); + assert.equal(index, numberOfBatchesProcessed++); + }; + + return eachAsync(next, fn, { batchSize }); + }); + + it('it processes the documents in batches even if the batch size % document count is not zero successfully', () => { + const batchSize = 3; + let numberOfDocuments = 0; + const maxNumberOfDocuments = 10; + let numberOfBatchesProcessed = 0; + + function next(cb) { + setTimeout(() => { + if (++numberOfDocuments > maxNumberOfDocuments) { + cb(null, null); + } + return cb(null, { id: numberOfDocuments }); + }, 0); + } + + const fn = (docs, index) => { + assert.equal(index, numberOfBatchesProcessed++); + if (index == 3) { + assert.equal(docs.length, 1); + } + else { + assert.equal(docs.length, batchSize); + } + }; + + return eachAsync(next, fn, { batchSize }); + }); + + it('it processes the documents in batches with the parallel option provided', () => { + const batchSize = 3; + const parallel = 3; + let numberOfDocuments = 0; + const maxNumberOfDocuments = 9; + let numberOfBatchesProcessed = 0; + + function next(cb) { + setTimeout(() => { + if (++numberOfDocuments > maxNumberOfDocuments) { + cb(null, null); + } + return cb(null, { id: numberOfDocuments }); + }, 0); + } + + const fn = (docs, index) => { + assert.equal(index, numberOfBatchesProcessed++); + assert.equal(docs.length, batchSize); + }; + + return eachAsync(next, fn, { batchSize, parallel }); + }); }); \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index 322fa4df39d..f4c178a812a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -530,8 +530,14 @@ describe('mongoose module:', function() { cb(); }); + const events = []; + mong.events.on('createConnection', conn => events.push(conn)); + const db2 = mong.createConnection(process.env.MONGOOSE_TEST_URI || uri, options); + assert.equal(events.length, 1); + assert.equal(events[0], db2); + db2.on('open', function() { connections++; cb(); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 4a897f477b0..a3f821c49e7 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -9983,4 +9983,181 @@ describe('model: populate:', function() { assert.deepEqual(posts[1].toObject().commentsIds.map(c => c.content), ['Im used in two posts', 'Nice second post']); }); }); + + it('supports `transform` option (gh-3375)', function() { + const parentSchema = new Schema({ + name: String, + children: [{ type: 'ObjectId', ref: 'Child' }], + child: { type: 'ObjectId', ref: 'Child' } + }); + const Parent = db.model('Parent', parentSchema); + + const Child = db.model('Child', Schema({ name: String })); + + return co(function*() { + const children = yield Child.create([{ name: 'Luke' }, { name: 'Leia' }]); + let p = yield Parent.create({ + name: 'Anakin', + children: children, + child: children[0]._id + }); + + let called = []; + function transform(doc, id) { + called.push({ + doc: doc, + id: id + }); + + return id; + } + + // Populate array of ids + p = yield Parent.findById(p).populate({ + path: 'children', + transform: transform + }); + + assert.equal(called.length, 2); + assert.equal(called[0].doc.name, 'Luke'); + assert.equal(called[0].id.toHexString(), children[0]._id.toHexString()); + + assert.equal(called[1].doc.name, 'Leia'); + assert.equal(called[1].id.toHexString(), children[1]._id.toHexString()); + + // Populate single id + called = []; + p = yield Parent.findById(p).populate({ + path: 'child', + transform: transform + }); + + assert.equal(called.length, 1); + assert.equal(called[0].doc.name, 'Luke'); + assert.equal(called[0].id.toHexString(), children[0]._id.toHexString()); + + // Push a nonexistent id + const newId = new mongoose.Types.ObjectId(); + yield Parent.updateOne({ _id: p._id }, { $push: { children: newId } }); + + called = []; + p = yield Parent.findById(p).populate({ + path: 'children', + transform: transform + }); + assert.equal(called.length, 3); + assert.strictEqual(called[2].doc, null); + assert.equal(called[2].id.toHexString(), newId.toHexString()); + + assert.equal(p.children[2].toHexString(), newId.toHexString()); + + // Populate 2 docs with same id + yield Parent.updateOne({ _id: p._id }, { $set: { children: [children[0], children[0]] } }); + called = []; + + p = yield Parent.findById(p).populate({ + path: 'children', + transform: transform + }); + assert.equal(called.length, 2); + assert.equal(called[0].id.toHexString(), children[0]._id.toHexString()); + assert.equal(called[1].id.toHexString(), children[0]._id.toHexString()); + + // Populate single id that points to nonexistent doc + yield Parent.updateOne({ _id: p._id }, { $set: { child: newId } }); + called = []; + p = yield Parent.findById(p).populate({ + path: 'child', + transform: transform + }); + + assert.equal(called.length, 1); + assert.strictEqual(called[0].doc, null); + assert.equal(called[0].id.toHexString(), newId.toHexString()); + }); + }); + + it('transform with virtual populate, justOne = true (gh-3375)', function() { + const parentSchema = new Schema({ + name: String + }); + parentSchema.virtual('child', { + ref: 'Child', + localField: '_id', + foreignField: 'parentId', + justOne: true + }); + const Parent = db.model('Parent', parentSchema); + + const Child = db.model('Child', Schema({ name: String, parentId: 'ObjectId' })); + + return co(function*() { + let p = yield Parent.create({ name: 'Anakin' }); + yield Child.create({ name: 'Luke', parentId: p._id }); + + const called = []; + + p = yield Parent.findById(p).populate({ + path: 'child', + transform: function(doc, id) { + called.push({ + doc: doc, + id: id + }); + + return id; + } + }); + + assert.equal(called.length, 1); + assert.strictEqual(called[0].doc.parentId.toHexString(), p._id.toHexString()); + assert.equal(called[0].id.toHexString(), p._id.toHexString()); + }); + }); + + it('transform with virtual populate, justOne = false (gh-3375)', function() { + const parentSchema = new Schema({ + name: String + }); + parentSchema.virtual('children', { + ref: 'Child', + localField: '_id', + foreignField: 'parentId', + justOne: false + }); + const Parent = db.model('Parent', parentSchema); + + const Child = db.model('Child', Schema({ name: String, parentId: 'ObjectId' })); + + return co(function*() { + let p = yield Parent.create({ name: 'Anakin' }); + yield Child.create([ + { name: 'Luke', parentId: p._id }, + { name: 'Leia', parentId: p._id } + ]); + + const called = []; + + p = yield Parent.findById(p).populate({ + path: 'children', + transform: function(doc, id) { + called.push({ + doc: doc, + id: id + }); + + return id; + } + }); + + assert.equal(called.length, 2); + assert.deepEqual(called.map(c => c.doc.name).sort(), ['Leia', 'Luke']); + + assert.strictEqual(called[0].doc.parentId.toHexString(), p._id.toHexString()); + assert.equal(called[0].id.toHexString(), p._id.toHexString()); + + assert.strictEqual(called[1].doc.parentId.toHexString(), p._id.toHexString()); + assert.equal(called[1].id.toHexString(), p._id.toHexString()); + }); + }); }); diff --git a/test/model.test.js b/test/model.test.js index 59bfc680911..818a23a79dc 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7083,4 +7083,26 @@ describe('Model', function() { }); }); }); + describe('Setting the explain flag', function() { + it('should give an object back rather than a boolean (gh-8275)', function() { + return co(function*() { + const MyModel = db.model('Character', mongoose.Schema({ + name: String, + age: Number, + rank: String + })); + + yield MyModel.create([ + { name: 'Jean-Luc Picard', age: 59, rank: 'Captain' }, + { name: 'William Riker', age: 29, rank: 'Commander' }, + { name: 'Deanna Troi', age: 28, rank: 'Lieutenant Commander' }, + { name: 'Geordi La Forge', age: 29, rank: 'Lieutenant' }, + { name: 'Worf', age: 24, rank: 'Lieutenant' } + ]); + const res = yield MyModel.exists({}, { explain: true }); + + assert.equal(typeof res, 'object'); + }); + }); + }); }); diff --git a/test/query.test.js b/test/query.test.js index bea3cef1226..ec3cbe670c9 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3724,4 +3724,21 @@ describe('Query', function() { assert.equal(quiz.questions[1].choices[0].choice_text, 'choice 1'); }); }); + + it('Query#pre() (gh-9784)', function() { + const Question = db.model('Test', Schema({ answer: Number })); + return co(function*() { + const q1 = Question.find({ answer: 42 }); + const called = []; + q1.pre(function middleware() { + called.push(this.getFilter()); + }); + yield q1.exec(); + assert.equal(called.length, 1); + assert.deepEqual(called[0], { answer: 42 }); + + yield Question.find({ answer: 42 }); + assert.equal(called.length, 1); + }); + }); }); diff --git a/test/schema.test.js b/test/schema.test.js index ee0d4e8a177..8dfe1b312a4 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1436,12 +1436,6 @@ describe('schema', function() { }); }, /`collection` may not be used as a schema pathname/); - assert.throws(function() { - new Schema({ - schema: String - }); - }, /`schema` may not be used as a schema pathname/); - assert.throws(function() { new Schema({ isNew: String