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.
Parameters
- fn
«Function»
- [options]
«Object»
- [options.parallel]
-«Number» the number of promises to execute in parallel. Defaults to 1.
- [callback]
+«Number» the number of promises to execute in parallel. Defaults to 1.
+«Object»
- [options.batchSize]
+ «Number» The size of documents processed by the passed in function to eachAsync (works with the parallel option)
+
[callback]
«Function» executed when all docs have been processedReturns:
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.
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