diff --git a/lib/connection.js b/lib/connection.js index 61d325a9dd2..797d4f50aef 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -491,6 +491,9 @@ Connection.prototype.transaction = function transaction(fn, options) { doc.set(doc.schema.options.versionKey, state.versionKey); } + if (state.modifiedPaths.length > 0 && doc.$__.activePaths.states.modify == null) { + doc.$__.activePaths.states.modify = {}; + } for (const path of state.modifiedPaths) { doc.$__.activePaths.paths[path] = 'modify'; doc.$__.activePaths.states.modify[path] = true; diff --git a/lib/document.js b/lib/document.js index 1c7b4f72537..4d228c3b82b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -19,6 +19,7 @@ const VirtualType = require('./virtualtype'); const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren'); const promiseOrCallback = require('./helpers/promiseOrCallback'); const castNumber = require('./cast/number'); +const applyDefaults = require('./helpers/document/applyDefaults'); const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths'); const compile = require('./helpers/document/compile').compile; const defineKey = require('./helpers/document/compile').defineKey; @@ -94,7 +95,11 @@ function Document(obj, fields, skipId, options) { } this.$__ = new InternalCache(); - this.$isNew = 'isNew' in options ? options.isNew : true; + + // Avoid setting `isNew` to `true`, because it is `true` by default + if (options.isNew != null && options.isNew !== true) { + this.$isNew = options.isNew; + } if (options.priorDoc != null) { this.$__.priorDoc = options.priorDoc; @@ -117,13 +122,12 @@ function Document(obj, fields, skipId, options) { const schema = this.$__schema; if (typeof fields === 'boolean' || fields === 'throw') { - this.$__.strictMode = fields; + if (fields !== true) { + this.$__.strictMode = fields; + } fields = undefined; - } else { + } else if (schema.options.strict !== true) { this.$__.strictMode = schema.options.strict; - if (fields != null) { - this.$__.selected = fields; - } } const requiredPaths = schema.requiredPaths(true); @@ -135,9 +139,9 @@ function Document(obj, fields, skipId, options) { // determine if this doc is a result of a query with // excluded fields - if (utils.isPOJO(fields)) { + if (utils.isPOJO(fields) && Object.keys(fields).length > 0) { exclude = isExclusive(fields); - this.$__.fields = fields; + this.$__.selected = fields; this.$__.exclude = exclude; } @@ -151,7 +155,7 @@ function Document(obj, fields, skipId, options) { // By default, defaults get applied **before** setting initial values // Re: gh-6155 if (defaults) { - $__applyDefaults(this, fields, exclude, hasIncludedChildren, true, null); + applyDefaults(this, fields, exclude, hasIncludedChildren, true, null); } } if (obj) { @@ -175,7 +179,7 @@ function Document(obj, fields, skipId, options) { this.$__.skipDefaults = options.skipDefaults; } } else if (defaults) { - $__applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults); + applyDefaults(this, fields, exclude, hasIncludedChildren, false, options.skipDefaults); } if (!this.$__.strictMode && obj) { @@ -211,6 +215,13 @@ Object.defineProperty(Document.prototype, 'errors', { this.$errors = value; } }); + +/*! + * ignore + */ + +Document.prototype.$isNew = true; + /*! * Document exposes the NodeJS event emitter API, so you can use * `on`, `once`, etc. @@ -442,122 +453,6 @@ Object.defineProperty(Document.prototype, '$op', { } }); -/*! - * ignore - */ - -function $__applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) { - const paths = Object.keys(doc.$__schema.paths); - const plen = paths.length; - - for (let i = 0; i < plen; ++i) { - let def; - let curPath = ''; - const p = paths[i]; - - if (p === '_id' && doc.$__.skipId) { - continue; - } - - const type = doc.$__schema.paths[p]; - const path = type.splitPath(); - const len = path.length; - let included = false; - let doc_ = doc._doc; - for (let j = 0; j < len; ++j) { - if (doc_ == null) { - break; - } - - const piece = path[j]; - curPath += (!curPath.length ? '' : '.') + piece; - - if (exclude === true) { - if (curPath in fields) { - break; - } - } else if (exclude === false && fields && !included) { - const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray; - if (curPath in fields || (hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) { - included = true; - } else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) { - break; - } - } - - if (j === len - 1) { - if (doc_[piece] !== void 0) { - break; - } - - if (typeof type.defaultValue === 'function') { - if (!type.defaultValue.$runBeforeSetters && isBeforeSetters) { - break; - } - if (type.defaultValue.$runBeforeSetters && !isBeforeSetters) { - break; - } - } else if (!isBeforeSetters) { - // Non-function defaults should always run **before** setters - continue; - } - - if (pathsToSkip && pathsToSkip[curPath]) { - break; - } - - if (fields && exclude !== null) { - if (exclude === true) { - // apply defaults to all non-excluded fields - if (p in fields) { - continue; - } - - try { - def = type.getDefault(doc, false); - } catch (err) { - doc.invalidate(p, err); - break; - } - - if (typeof def !== 'undefined') { - doc_[piece] = def; - doc.$__.activePaths.default(p); - } - } else if (included) { - // selected field - try { - def = type.getDefault(doc, false); - } catch (err) { - doc.invalidate(p, err); - break; - } - - if (typeof def !== 'undefined') { - doc_[piece] = def; - doc.$__.activePaths.default(p); - } - } - } else { - try { - def = type.getDefault(doc, false); - } catch (err) { - doc.invalidate(p, err); - break; - } - - if (typeof def !== 'undefined') { - doc_[piece] = def; - doc.$__.activePaths.default(p); - } - } - } else { - doc_ = doc_[piece]; - } - } - } -} - /*! * ignore */ @@ -774,10 +669,11 @@ Document.prototype.$__init = function(doc, opts) { this.$emit('init', this); this.constructor.emit('init', this); - const hasIncludedChildren = this.$__.exclude === false && this.$__.fields ? - $__hasIncludedChildren(this.$__.fields) : + const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ? + $__hasIncludedChildren(this.$__.selected) : null; - $__applyDefaults(this, this.$__.fields, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); + + applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); return this; }; @@ -845,7 +741,13 @@ function init(self, obj, doc, opts, prefix) { if (schemaType && !wasPopulated) { try { - doc[i] = schemaType.cast(obj[i], self, true); + if (opts && opts.setters) { + // Call applySetters with `init = false` because otherwise setters are a noop + const overrideInit = false; + doc[i] = schemaType.applySetters(obj[i], self, overrideInit); + } else { + doc[i] = schemaType.cast(obj[i], self, true); + } } catch (e) { self.invalidate(e.path, new ValidatorError({ path: e.path, @@ -1621,7 +1523,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru return true; } - if (val === void 0 && path in this.$__.activePaths.states.default) { + if (val === void 0 && path in this.$__.activePaths.getStatePaths('default')) { // we're just unsetting the default value which was never saved return false; } @@ -1641,7 +1543,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru if (!constructing && val !== null && val !== undefined && - path in this.$__.activePaths.states.default && + path in this.$__.activePaths.getStatePaths('default') && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server // and the user is setting it to the same value again @@ -2007,7 +1909,7 @@ Document.prototype.$ignore = function(path) { */ Document.prototype.directModifiedPaths = function() { - return Object.keys(this.$__.activePaths.states.modify); + return Object.keys(this.$__.activePaths.getStatePaths('modify')); }; /** @@ -2085,7 +1987,7 @@ function _isEmpty(v) { Document.prototype.modifiedPaths = function(options) { options = options || {}; - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); const result = new Set(); let i = 0; @@ -2164,7 +2066,7 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; Document.prototype.isModified = function(paths, modifiedPaths) { if (paths) { - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); if (directModifiedPaths.length === 0) { return false; } @@ -2214,7 +2116,7 @@ Document.prototype.$isDefault = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.default.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('default').hasOwnProperty(path); } let paths = path; @@ -2222,7 +2124,7 @@ Document.prototype.$isDefault = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.default.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('default').hasOwnProperty(path)); }; /** @@ -2275,7 +2177,7 @@ Document.prototype.isDirectModified = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.modify.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path); } let paths = path; @@ -2283,7 +2185,7 @@ Document.prototype.isDirectModified = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.modify.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path)); }; /** @@ -2300,7 +2202,7 @@ Document.prototype.isInit = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.init.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('init').hasOwnProperty(path); } let paths = path; @@ -2308,7 +2210,7 @@ Document.prototype.isInit = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.init.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('init').hasOwnProperty(path)); }; /** @@ -2536,7 +2438,7 @@ Document.prototype.$validate = Document.prototype.validate; */ function _evaluateRequiredFunctions(doc) { - const requiredFields = Object.keys(doc.$__.activePaths.states.require); + const requiredFields = Object.keys(doc.$__.activePaths.getStatePaths('require')); let i = 0; const len = requiredFields.length; for (i = 0; i < len; ++i) { @@ -2564,7 +2466,7 @@ function _getPathsToValidate(doc) { _evaluateRequiredFunctions(doc); // only validate required fields when necessary - let paths = new Set(Object.keys(doc.$__.activePaths.states.require).filter(function(path) { + let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) { if (!doc.$__isSelected(path) && !doc.$isModified(path)) { return false; } @@ -2574,9 +2476,9 @@ function _getPathsToValidate(doc) { return true; })); - Object.keys(doc.$__.activePaths.states.init).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.modify).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('init')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('modify')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths); function addToPaths(p) { paths.add(p); } const subdocs = doc.$getAllSubdocs(); @@ -3298,8 +3200,8 @@ Document.prototype.$__reset = function reset() { this.$__.backup = {}; this.$__.backup.activePaths = { - modify: Object.assign({}, this.$__.activePaths.states.modify), - default: Object.assign({}, this.$__.activePaths.states.default) + modify: Object.assign({}, this.$__.activePaths.getStatePaths('modify')), + default: Object.assign({}, this.$__.activePaths.getStatePaths('default')) }; this.$__.backup.validationError = this.$__.validationError; this.$__.backup.errors = this.$errors; diff --git a/lib/helpers/document/applyDefaults.js b/lib/helpers/document/applyDefaults.js new file mode 100644 index 00000000000..82a092df292 --- /dev/null +++ b/lib/helpers/document/applyDefaults.js @@ -0,0 +1,115 @@ +'use strict'; + +module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) { + const paths = Object.keys(doc.$__schema.paths); + const plen = paths.length; + + for (let i = 0; i < plen; ++i) { + let def; + let curPath = ''; + const p = paths[i]; + + if (p === '_id' && doc.$__.skipId) { + continue; + } + + const type = doc.$__schema.paths[p]; + const path = type.splitPath(); + const len = path.length; + let included = false; + let doc_ = doc._doc; + for (let j = 0; j < len; ++j) { + if (doc_ == null) { + break; + } + + const piece = path[j]; + curPath += (!curPath.length ? '' : '.') + piece; + + if (exclude === true) { + if (curPath in fields) { + break; + } + } else if (exclude === false && fields && !included) { + const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray; + if (curPath in fields || (hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) { + included = true; + } else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) { + break; + } + } + + if (j === len - 1) { + if (doc_[piece] !== void 0) { + break; + } + + if (isBeforeSetters != null) { + if (typeof type.defaultValue === 'function') { + if (!type.defaultValue.$runBeforeSetters && isBeforeSetters) { + break; + } + if (type.defaultValue.$runBeforeSetters && !isBeforeSetters) { + break; + } + } else if (!isBeforeSetters) { + // Non-function defaults should always run **before** setters + continue; + } + } + + if (pathsToSkip && pathsToSkip[curPath]) { + break; + } + + if (fields && exclude !== null) { + if (exclude === true) { + // apply defaults to all non-excluded fields + if (p in fields) { + continue; + } + + try { + def = type.getDefault(doc, false); + } catch (err) { + doc.invalidate(p, err); + break; + } + + if (typeof def !== 'undefined') { + doc_[piece] = def; + doc.$__.activePaths.default(p); + } + } else if (included) { + // selected field + try { + def = type.getDefault(doc, false); + } catch (err) { + doc.invalidate(p, err); + break; + } + + if (typeof def !== 'undefined') { + doc_[piece] = def; + doc.$__.activePaths.default(p); + } + } + } else { + try { + def = type.getDefault(doc, false); + } catch (err) { + doc.invalidate(p, err); + break; + } + + if (typeof def !== 'undefined') { + doc_[piece] = def; + doc.$__.activePaths.default(p); + } + } + } else { + doc_ = doc_[piece]; + } + } + } +}; diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js index 29bd18c3ac9..43c225e4fd2 100644 --- a/lib/helpers/document/cleanModifiedSubpaths.js +++ b/lib/helpers/document/cleanModifiedSubpaths.js @@ -13,7 +13,7 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { return deleted; } - for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) { + for (const modifiedPath of Object.keys(doc.$__.activePaths.getStatePaths('modify'))) { if (skipDocArrays) { const schemaType = doc.$__schema.path(modifiedPath); if (schemaType && schemaType.$isMongooseDocumentArray) { @@ -21,13 +21,13 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { } } if (modifiedPath.startsWith(path + '.')) { - delete doc.$__.activePaths.states.modify[modifiedPath]; + doc.$__.activePaths.clearPath(modifiedPath); ++deleted; if (doc.$isSubdocument) { const owner = doc.ownerDocument(); const fullPath = doc.$__fullPath(modifiedPath); - delete owner.$__.activePaths.states.modify[fullPath]; + owner.$__.activePaths.clearPath(fullPath); } } } diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js index 80a0de0b041..71597582535 100644 --- a/lib/helpers/document/compile.js +++ b/lib/helpers/document/compile.js @@ -17,6 +17,13 @@ const isPOJO = utils.isPOJO; exports.compile = compile; exports.defineKey = defineKey; +const _isEmptyOptions = Object.freeze({ + minimize: true, + virtuals: false, + getters: false, + transform: false +}); + /*! * Compiles schemas. */ @@ -130,12 +137,6 @@ function defineKey({ prop, subprops, prototype, prefix, options }) { value: true }); - const _isEmptyOptions = Object.freeze({ - minimize: true, - virtuals: false, - getters: false, - transform: false - }); Object.defineProperty(nested, '$isEmpty', { enumerable: false, configurable: true, diff --git a/lib/helpers/model/applyDefaultsToPOJO.js b/lib/helpers/model/applyDefaultsToPOJO.js new file mode 100644 index 00000000000..4aca295cd29 --- /dev/null +++ b/lib/helpers/model/applyDefaultsToPOJO.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = function applyDefaultsToPOJO(doc, schema) { + const paths = Object.keys(schema.paths); + const plen = paths.length; + + for (let i = 0; i < plen; ++i) { + let curPath = ''; + const p = paths[i]; + + const type = schema.paths[p]; + const path = type.splitPath(); + const len = path.length; + let doc_ = doc; + for (let j = 0; j < len; ++j) { + if (doc_ == null) { + break; + } + + const piece = path[j]; + curPath += (!curPath.length ? '' : '.') + piece; + + if (j === len - 1) { + if (typeof doc_[piece] !== 'undefined') { + if (type.$isSingleNested) { + applyDefaultsToPOJO(doc_[piece], type.caster.schema); + } else if (type.$isMongooseDocumentArray && Array.isArray(doc_[piece])) { + doc_[piece].forEach(el => applyDefaultsToPOJO(el, type.schema)); + } + + break; + } + + const def = type.getDefault(doc, false, { skipCast: true }); + if (typeof def !== 'undefined') { + doc_[piece] = def; + + if (type.$isSingleNested) { + applyDefaultsToPOJO(def, type.caster.schema); + } else if (type.$isMongooseDocumentArray && Array.isArray(def)) { + def.forEach(el => applyDefaultsToPOJO(el, type.schema)); + } + } + } else { + if (doc_[piece] == null) { + doc_[piece] = {}; + } + doc_ = doc_[piece]; + } + } + } +}; diff --git a/lib/helpers/timestamps/setDocumentTimestamps.js b/lib/helpers/timestamps/setDocumentTimestamps.js new file mode 100644 index 00000000000..c1b6d5fc2c1 --- /dev/null +++ b/lib/helpers/timestamps/setDocumentTimestamps.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = function setDocumentTimestamps(doc, timestampOption, currentTime, createdAt, updatedAt) { + const skipUpdatedAt = timestampOption != null && timestampOption.updatedAt === false; + const skipCreatedAt = timestampOption != null && timestampOption.createdAt === false; + + const defaultTimestamp = currentTime != null ? + currentTime() : + doc.ownerDocument().constructor.base.now(); + + if (!skipCreatedAt && + (doc.isNew || doc.$isSubdocument) && + createdAt && + !doc.$__getValue(createdAt) && + doc.$__isSelected(createdAt)) { + doc.$set(createdAt, defaultTimestamp, undefined, { overwriteImmutable: true }); + } + + if (!skipUpdatedAt && updatedAt && (doc.isNew || doc.$isModified())) { + let ts = defaultTimestamp; + if (doc.isNew && createdAt != null) { + ts = doc.$__getValue(createdAt); + } + doc.$set(updatedAt, ts); + } +}; diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index c254365ac3e..06c57ea74f8 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -4,6 +4,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren') const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate'); const get = require('../get'); const handleTimestampOption = require('../schema/handleTimestampOption'); +const setDocumentTimestamps = require('./setDocumentTimestamps'); const symbols = require('../../schema/symbols'); module.exports = function setupTimestamps(schema, timestamps) { @@ -44,24 +45,7 @@ module.exports = function setupTimestamps(schema, timestamps) { return next(); } - const skipUpdatedAt = timestampOption != null && timestampOption.updatedAt === false; - const skipCreatedAt = timestampOption != null && timestampOption.createdAt === false; - - const defaultTimestamp = currentTime != null ? - currentTime() : - this.ownerDocument().constructor.base.now(); - - if (!skipCreatedAt && (this.isNew || this.$isSubdocument) && createdAt && !this.$__getValue(createdAt) && this.$__isSelected(createdAt)) { - this.$set(createdAt, defaultTimestamp, undefined, { overwriteImmutable: true }); - } - - if (!skipUpdatedAt && updatedAt && (this.isNew || this.$isModified())) { - let ts = defaultTimestamp; - if (this.isNew && createdAt != null) { - ts = this.$__getValue(createdAt); - } - this.$set(updatedAt, ts); - } + setDocumentTimestamps(this, timestampOption, currentTime, createdAt, updatedAt); next(); }); @@ -76,6 +60,18 @@ module.exports = function setupTimestamps(schema, timestamps) { if (updatedAt && !this.get(updatedAt)) { this.$set(updatedAt, ts); } + + if (this.$isSubdocument) { + return this; + } + + const subdocs = this.$getAllSubdocs(); + for (const subdoc of subdocs) { + if (subdoc.initializeTimestamps) { + subdoc.initializeTimestamps(); + } + } + return this; }; diff --git a/lib/helpers/topology/isAtlas.js b/lib/helpers/topology/isAtlas.js index 42ed211fa53..445a8b49cd3 100644 --- a/lib/helpers/topology/isAtlas.js +++ b/lib/helpers/topology/isAtlas.js @@ -2,25 +2,30 @@ const getConstructorName = require('../getConstructorName'); +/** + * @typedef { import('mongodb').TopologyDescription } TopologyDescription + */ + +/** + * Checks if topologyDescription contains servers connected to an atlas instance + * + * @param {TopologyDescription} topologyDescription + * @returns {boolean} + */ module.exports = function isAtlas(topologyDescription) { if (getConstructorName(topologyDescription) !== 'TopologyDescription') { return false; } - const hostnames = Array.from(topologyDescription.servers.keys()); - - if (hostnames.length === 0) { + if (topologyDescription.servers.size === 0) { return false; } - for (let i = 0, il = hostnames.length; i < il; ++i) { - const url = new URL(hostnames[i]); - if ( - url.hostname.endsWith('.mongodb.net') === false || - url.port !== '27017' - ) { + for (const server of topologyDescription.servers.values()) { + if (server.host.endsWith('.mongodb.net') === false || server.port !== 27017) { return false; } } + return true; }; diff --git a/lib/index.js b/lib/index.js index 69bd2de280f..480afd45b2c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -204,6 +204,7 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * - 'overwriteModels': Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`. * - 'returnOriginal': If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](/docs/tutorials/findoneandupdate.html) for more information. * - 'runValidators': `false` by default. Set to true to enable [update validators](/docs/validation.html#update-validators) for all validators by default. + * - 'sanitizeFilter': `false` by default. Set to true to enable the [sanitization of the query filters](/docs/api.html#mongoose_Mongoose-sanitizeFilter) against query selector injection attacks by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`. * - 'selectPopulatedPaths': `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one. * - 'strict': `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas. * - 'strictQuery': same value as 'strict' by default (`true`), may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas. diff --git a/lib/internal.js b/lib/internal.js index 9cd55ef9d8b..c4445c254d6 100644 --- a/lib/internal.js +++ b/lib/internal.js @@ -13,8 +13,9 @@ function InternalCache() { this.activePaths = new ActiveRoster(); } +InternalCache.prototype.strictMode = true; + InternalCache.prototype.fullPath = undefined; -InternalCache.prototype.strictMode = undefined; InternalCache.prototype.selected = undefined; InternalCache.prototype.shardval = undefined; InternalCache.prototype.saveError = undefined; diff --git a/lib/model.js b/lib/model.js index 78dfa03701b..36eb73bbdab 100644 --- a/lib/model.js +++ b/lib/model.js @@ -22,6 +22,8 @@ const ServerSelectionError = require('./error/serverSelection'); const ValidationError = require('./error/validation'); const VersionError = require('./error/version'); const ParallelSaveError = require('./error/parallelSave'); +const applyDefaultsHelper = require('./helpers/document/applyDefaults'); +const applyDefaultsToPOJO = require('./helpers/model/applyDefaultsToPOJO'); const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware'); const applyHooks = require('./helpers/model/applyHooks'); const applyMethods = require('./helpers/model/applyMethods'); @@ -97,7 +99,7 @@ const saveToObjectOptions = Object.assign({}, internalToObjectOptions, { * const userFromDb = await UserModel.findOne({ name: 'Foo' }); * * @param {Object} doc values for initial set - * @param [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](./api.html#query_Query-select). + * @param {Object} [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](./api.html#query_Query-select). * @param {Boolean} [skipId=false] optional boolean. If true, mongoose doesn't add an `_id` field to the document. * @inherits Document https://mongoosejs.com/docs/api/document.html * @event `error`: If listening to this event, 'error' is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. @@ -211,6 +213,7 @@ Model.prototype.baseModelName; * await MyModel.findOne({ _id: 'Not a valid ObjectId' }).catch(noop); * * @api public + * @property events * @fires error whenever any query or model function errors * @memberOf Model * @static @@ -896,26 +899,30 @@ Model.prototype.$__version = function(where, delta) { } }; +/*! + * ignore + */ + +function increment() { + this.$__.version = VERSION_ALL; + return this; +} + /** * Signal that we desire an increment of this documents version. * * #### Example: * - * Model.findById(id, function (err, doc) { - * doc.increment(); - * doc.save(function (err) { .. }) - * }) + * const doc = await Model.findById(id); + * doc.increment(); + * await doc.save(); * * @see versionKeys https://mongoosejs.com/docs/guide.html#versionKey * @memberOf Model + * @method increment * @api public */ -function increment() { - this.$__.version = VERSION_ALL; - return this; -} - Model.prototype.increment = increment; /** @@ -945,22 +952,12 @@ Model.prototype.$__where = function _where(where) { * Removes this document from the db. * * #### Example: - * product.remove(function (err, product) { - * if (err) return handleError(err); - * Product.findById(product._id, function (err, product) { - * console.log(product) // null - * }) - * }) - * * - * As an extra measure of flow control, remove will return a Promise (bound to `fn` if passed) so it could be chained, or hooked to receive errors - * - * #### Example: - * product.remove().then(function (product) { - * ... - * }).catch(function (err) { - * assert.ok(err) - * }) + * const product = await product.remove().catch(function (err) { + * assert.ok(err); + * }); + * const foundProduct = await Product.findById(product._id); + * console.log(foundProduct) // null * * @param {Object} [options] * @param {Session} [options.session=null] the [session](https://docs.mongodb.com/manual/reference/server-sessions/) associated with this operation. If not specified, defaults to the [document's associated session](api.html#document_Document-$session). @@ -1003,6 +1000,7 @@ Model.prototype.delete = Model.prototype.remove; * Removes this document from the db. Equivalent to `.remove()`. * * #### Example: + * * product = await product.deleteOne(); * await Product.findById(product._id); // null * @@ -1112,6 +1110,7 @@ Model.prototype.$model = function $model(name) { * `MyModel.findOne({ answer: 42 }).select({ _id: 1 }).lean()` * * #### Example: + * * await Character.deleteMany({}); * await Character.create({ name: 'Jean-Luc Picard' }); * @@ -1281,7 +1280,7 @@ for (const i in EventEmitter.prototype) { * * #### Example: * - * const eventSchema = new Schema({ thing: { type: 'string', unique: true }}) + * const eventSchema = new Schema({ thing: { type: 'string', unique: true } }) * // This calls `Event.init()` implicitly, so you don't need to call * // `Event.init()` on your own. * const Event = mongoose.model('Event', eventSchema); @@ -1499,7 +1498,7 @@ Model.syncIndexes = function syncIndexes(options, callback) { * Model.syncIndexes(). * * @param {Object} [options] - * @param {Function} callback optional callback + * @param {Function} [callback] optional callback * @returns {Promise} which contains an object, {toDrop, toCreate}, which * are indexes that would be dropped in MongoDB and indexes that would be created in MongoDB. */ @@ -1663,7 +1662,7 @@ Model.listIndexes = function init(callback) { * * #### Example: * - * const eventSchema = new Schema({ thing: { type: 'string', unique: true }}) + * const eventSchema = new Schema({ thing: { type: 'string', unique: true } }) * const Event = mongoose.model('Event', eventSchema); * * Event.on('index', function (err) { @@ -1898,6 +1897,7 @@ Model.discriminators; * .exec(function(err, characters) {}) * * #### Note: + * * Only translate arguments of object type anything else is returned raw * * @param {Object} fields fields/conditions that may contain aliased keys @@ -2394,7 +2394,7 @@ Model.count = function count(conditions, callback) { * * #### Example: * - * Link.distinct('url', { clicks: {$gt: 100}}, function (err, result) { + * Link.distinct('url', { clicks: { $gt: 100 } }, function (err, result) { * if (err) return handleError(err); * * assert(Array.isArray(result)); @@ -2430,7 +2430,7 @@ Model.distinct = function distinct(field, conditions, callback) { * * For example, instead of writing: * - * User.find({age: {$gte: 21, $lte: 65}}, callback); + * User.find({ age: { $gte: 21, $lte: 65 } }, callback); * * we can instead write: * @@ -2484,19 +2484,6 @@ Model.$where = function $where() { * * Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes if `callback` is passed else a Query object is returned. * - * #### Options: - * - * - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0) - * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. - * - `overwrite`: bool - if true, replace the entire document. - * - `fields`: {Object|String} - Field selection. Equivalent to `.select(fields).findOneAndUpdate()` - * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. - * - `setDefaultsOnInsert`: `true` by default. If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findOneAndUpdate(conditions, update, options, callback) // executes @@ -2542,6 +2529,13 @@ Model.$where = function $where() { * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://docs.mongodb.com/manual/reference/operator/update/) in `update`, Mongoose will wrap `update` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`. An alternative to this would be using [Model.findOneAndReplace(conditions, update, options, callback)](https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndReplace). * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select) + * @param {Boolean} [options.new=false] if true, return the modified document rather than the original + * @param {Object|String} [options.fields] Field selection. Equivalent to `.select(fields).findOneAndUpdate()` + * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Boolean} [options.runValidators] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema + * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) * @param {Function} [callback] * @return {Query} * @see Tutorial /docs/tutorials/findoneandupdate.html @@ -2622,17 +2616,6 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) { * * - `findOneAndUpdate()` * - * #### Options: - * - * - `new`: bool - true to return the modified document rather than the original. defaults to false - * - `upsert`: bool - creates the object if it doesn't exist. defaults to false. - * - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema. - * - `setDefaultsOnInsert`: `true` by default. If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `select`: sets the document fields to return - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findByIdAndUpdate(id, update, options, callback) // executes @@ -2662,11 +2645,9 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) { * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * - * Model.findById(id, function (err, doc) { - * if (err) .. - * doc.name = 'jason bourne'; - * doc.save(callback); - * }); + * const doc = await Model.findById(id) + * doc.name = 'jason bourne'; + * await doc.save(); * * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [update] @@ -2677,6 +2658,13 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) { * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://docs.mongodb.com/manual/reference/operator/update/) in `update`, Mongoose will wrap `update` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`. An alternative to this would be using [Model.findOneAndReplace({ _id: id }, update, options, callback)](https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndReplace). + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Boolean} [options.runValidators] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema + * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) + * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document + * @param {Boolean} [options.new=false] if true, return the modified document rather than the original + * @param {Object|String} [options.select] sets the document fields to return. * @param {Function} [callback] * @return {Query} * @see Model.findOneAndUpdate #model_Model.findOneAndUpdate @@ -2725,15 +2713,6 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { * this distinction is purely pedantic. You should use `findOneAndDelete()` * unless you have a good reason not to. * - * #### Options: - * - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 - * - `select`: sets the document fields to return, ex. `{ projection: { _id: 0 } }` - * - `projection`: equivalent to `select` - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findOneAndDelete(conditions, options, callback) // executes @@ -2748,17 +2727,19 @@ Model.findByIdAndUpdate = function(id, update, options, callback) { * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * - * Model.findById(id, function (err, doc) { - * if (err) .. - * doc.name = 'jason bourne'; - * doc.save(callback); - * }); + * const doc = await Model.findById(id) + * doc.name = 'jason bourne'; + * await doc.save(); * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html). + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Object|String} [options.select] sets the document fields to return. + * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Function} [callback] * @return {Query} * @api public @@ -2838,15 +2819,6 @@ Model.findByIdAndDelete = function(id, options, callback) { * * - `findOneAndReplace()` * - * #### Options: - * - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 - * - `select`: sets the document fields to return - * - `projection`: like select, it determines which fields to return, ex. `{ projection: { _id: 0 } }` - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findOneAndReplace(filter, replacement, options, callback) // executes @@ -2864,6 +2836,10 @@ Model.findByIdAndDelete = function(id, options, callback) { * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select) + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) + * @param {Object|String} [options.select] sets the document fields to return. + * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Function} [callback] * @return {Query} * @api public @@ -2917,15 +2893,6 @@ Model.findOneAndReplace = function(filter, replacement, options, callback) { * * - `findOneAndRemove()` * - * #### Options: - * - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 - * - `select`: sets the document fields to return - * - `projection`: like select, it determines which fields to return, ex. `{ projection: { _id: 0 } }` - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findOneAndRemove(conditions, options, callback) // executes @@ -2940,17 +2907,19 @@ Model.findOneAndReplace = function(filter, replacement, options, callback) { * If you need full-fledged validation, use the traditional approach of first * retrieving the document. * - * Model.findById(id, function (err, doc) { - * if (err) .. - * doc.name = 'jason bourne'; - * doc.save(callback); - * }); + * const doc = await Model.findById(id); + * doc.name = 'jason bourne'; + * await doc.save(); * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api.html#query_Query-setOptions) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html). * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select) + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) + * @param {Object|String} [options.select] sets the document fields to return. + * @param {Number} [options.maxTimeMS] puts a time limit on the query - requires mongodb >= 2.6.0 * @param {Function} [callback] * @return {Query} * @see mongodb https://www.mongodb.org/display/DOCS/findAndModify+Command @@ -2997,13 +2966,6 @@ Model.findOneAndRemove = function(conditions, options, callback) { * * - `findOneAndRemove()` * - * #### Options: - * - * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update - * - `select`: sets the document fields to return - * - `rawResult`: if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) - * - `strict`: overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) for this update - * * #### Examples: * * A.findByIdAndRemove(id, options, callback) // executes @@ -3017,6 +2979,9 @@ Model.findOneAndRemove = function(conditions, options, callback) { * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](/docs/transactions.html). * @param {Object|String|String[]} [options.projection=null] optional fields to return, see [`Query.prototype.select()`](#query_Query-select) + * @param {Object|String} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update. + * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.3/interfaces/ModifyResult.html) + * @param {Object|String} [options.select] sets the document fields to return. * @param {Function} [callback] * @return {Query} * @see Model.findOneAndRemove #model_Model.findOneAndRemove @@ -3404,7 +3369,7 @@ Model.$__insertMany = function(arr, options, callback) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } - if (doc.initializeTimestamps) { + if ((!options || options.timestamps !== false) && doc.initializeTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); } return doc.toObject(internalToObjectOptions); @@ -3700,6 +3665,30 @@ function handleSuccessfulWrite(document) { }); } +/** + * Apply defaults to the given document or POJO. + * + * @param {Object|Document} obj object or document to apply defaults on + * @returns {Object|Document} + * @api public + */ + +Model.applyDefaults = function applyDefaults(doc) { + if (doc.$__ != null) { + applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude); + + for (const subdoc of doc.$getAllSubdocs()) { + applyDefaults(subdoc, subdoc.$__.fields, subdoc.$__.exclude); + } + + return doc; + } + + applyDefaultsToPOJO(doc, this.schema); + + return doc; +}; + /** * Build bulk write operations for `bulkSave()`. * @@ -3710,6 +3699,7 @@ function handleSuccessfulWrite(document) { * @return {Array} Returns a array of all Promises the function executes to be awaited. * @api private */ + Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) { if (!Array.isArray(documents)) { throw new Error(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`); @@ -3780,11 +3770,13 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op * * @param {Object} obj * @param {Object|String|String[]} [projection] optional projection containing which fields should be selected for this document + * @param {Object} [options] optional options + * @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating * @return {Document} document instance * @api public */ -Model.hydrate = function(obj, projection) { +Model.hydrate = function(obj, projection, options) { _checkContext(this, 'hydrate'); if (projection != null) { @@ -3795,7 +3787,7 @@ Model.hydrate = function(obj, projection) { } const document = require('./queryhelpers').createModel(this, obj, projection); - document.$init(obj); + document.$init(obj, options); return document; }; @@ -3870,7 +3862,6 @@ Model.hydrate = function(obj, projection) { * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://docs.mongodb.com/manual/reference/operator/update/) in `doc`, Mongoose will wrap `doc` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`. * @param {Function} [callback] params are (error, [updateWriteOpResult](https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#~updateWriteOpResult)) - * @param {Function} [callback] * @return {Query} * @see MongoDB docs https://docs.mongodb.com/manual/reference/command/update/#update-command-output * @see writeOpResult https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#~updateWriteOpResult @@ -4036,54 +4027,34 @@ function _update(model, op, conditions, doc, options, callback) { /** * Executes a mapReduce command. * - * `o` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation. See [node-mongodb-native mapReduce() documentation](https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#mapreduce) for more detail about options. + * `opts` is an object specifying all mapReduce options as well as the map and reduce functions. All options are delegated to the driver implementation. See [node-mongodb-native mapReduce() documentation](https://mongodb.github.io/node-mongodb-native/api-generated/collection.html#mapreduce) for more detail about options. * * This function does not trigger any middleware. * * #### Example: * - * const o = {}; + * const opts = {}; * // `map()` and `reduce()` are run on the MongoDB server, not Node.js, * // these functions are converted to strings - * o.map = function () { emit(this.name, 1) }; - * o.reduce = function (k, vals) { return vals.length }; - * User.mapReduce(o, function (err, results) { + * opts.map = function () { emit(this.name, 1) }; + * opts.reduce = function (k, vals) { return vals.length }; + * User.mapReduce(opts, function (err, results) { * console.log(results) * }) * - * #### Other options: - * - * - `query` {Object} query filter object. - * - `sort` {Object} sort input objects using this key - * - `limit` {Number} max number of documents - * - `keeptemp` {Boolean, default:false} keep temporary data - * - `finalize` {Function} finalize function - * - `scope` {Object} scope variables exposed to map/reduce/finalize during execution - * - `jsMode` {Boolean, default:false} it is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X - * - `verbose` {Boolean, default:false} provide statistics on job execution time. - * - `readPreference` {String} - * - `out*` {Object, default: {inline:1}} sets the output target for the map reduce job. - * - * #### * out options: - * - * - `{inline:1}` the results are returned in an array - * - `{replace: 'collectionName'}` add the results to collectionName: the results replace the collection - * - `{reduce: 'collectionName'}` add the results to collectionName: if dups are detected, uses the reducer / finalize functions - * - `{merge: 'collectionName'}` add the results to collectionName: if dups exist the new docs overwrite the old - * - * If `options.out` is set to `replace`, `merge`, or `reduce`, a Model instance is returned that can be used for further querying. Queries run against this model are all executed with the [`lean` option](/docs/tutorials/lean.html); meaning only the js object is returned and no Mongoose magic is applied (getters, setters, etc). + * If `opts.out` is set to `replace`, `merge`, or `reduce`, a Model instance is returned that can be used for further querying. Queries run against this model are all executed with the [`lean` option](/docs/tutorials/lean.html); meaning only the js object is returned and no Mongoose magic is applied (getters, setters, etc). * * #### Example: * - * const o = {}; + * const opts = {}; * // You can also define `map()` and `reduce()` as strings if your * // linter complains about `emit()` not being defined - * o.map = 'function () { emit(this.name, 1) }'; - * o.reduce = 'function (k, vals) { return vals.length }'; - * o.out = { replace: 'createdCollectionNameForResults' } - * o.verbose = true; + * opts.map = 'function () { emit(this.name, 1) }'; + * opts.reduce = 'function (k, vals) { return vals.length }'; + * opts.out = { replace: 'createdCollectionNameForResults' } + * opts.verbose = true; * - * User.mapReduce(o, function (err, model, stats) { + * User.mapReduce(opts, function (err, model, stats) { * console.log('map reduce took %d ms', stats.processtime) * model.find().where('value').gt(10).exec(function (err, docs) { * console.log(docs); @@ -4092,8 +4063,8 @@ function _update(model, op, conditions, doc, options, callback) { * * // `mapReduce()` returns a promise. However, ES6 promises can only * // resolve to exactly one value, - * o.resolveToObject = true; - * const promise = User.mapReduce(o); + * opts.resolveToObject = true; + * const promise = User.mapReduce(opts); * promise.then(function (res) { * const model = res.model; * const stats = res.stats; @@ -4103,14 +4074,28 @@ function _update(model, op, conditions, doc, options, callback) { * console.log(docs); * }).then(null, handleError).end() * - * @param {Object} o an object specifying map-reduce options + * @param {Object} opts an object specifying map-reduce options + * @param {Boolean} [opts.verbose=false] provide statistics on job execution time + * @param {ReadPreference|String} [opts.readPreference] a read-preference string or a read-preference instance + * @param {Boolean} [opts.jsMode=false] it is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X + * @param {Object} [opts.scope] scope variables exposed to map/reduce/finalize during execution + * @param {Function} [opts.finalize] finalize function + * @param {Boolean} [opts.keeptemp=false] keep temporary data + * @param {Number} [opts.limit] max number of documents + * @param {Object} [opts.sort] sort input objects using this key + * @param {Object} [opts.query] query filter object + * @param {Object} [opts.out] sets the output target for the map reduce job + * @param {Number} [opts.out.inline=1] the results are returned in an array + * @param {String} [opts.out.replace] add the results to collectionName: the results replace the collection + * @param {String} [opts.out.reduce] add the results to collectionName: if dups are detected, uses the reducer / finalize functions + * @param {String} [opts.out.merge] add the results to collectionName: if dups exist the new docs overwrite the old * @param {Function} [callback] optional callback * @see https://www.mongodb.org/display/DOCS/MapReduce * @return {Promise} * @api public */ -Model.mapReduce = function mapReduce(o, callback) { +Model.mapReduce = function mapReduce(opts, callback) { _checkContext(this, 'mapReduce'); callback = this.$handleCallbackError(callback); @@ -4123,20 +4108,20 @@ Model.mapReduce = function mapReduce(o, callback) { Model.mapReduce.schema = new Schema({}, opts); } - if (!o.out) o.out = { inline: 1 }; - if (o.verbose !== false) o.verbose = true; + if (!opts.out) opts.out = { inline: 1 }; + if (opts.verbose !== false) opts.verbose = true; - o.map = String(o.map); - o.reduce = String(o.reduce); + opts.map = String(opts.map); + opts.reduce = String(opts.reduce); - if (o.query) { - let q = new this.Query(o.query); + if (opts.query) { + let q = new this.Query(opts.query); q.cast(this); - o.query = q._conditions; + opts.query = q._conditions; q = undefined; } - this.$__collection.mapReduce(null, null, o, (err, res) => { + this.$__collection.mapReduce(null, null, opts, (err, res) => { if (err) { return cb(err); } diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index e0887d65c71..a1726e8ca86 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -23,14 +23,14 @@ module.exports = function trackTransaction(schema) { initialState.versionKey = this.get(this.$__schema.options.versionKey); } - initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.states.modify)); + initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.getStatePaths('modify'))); initialState.atomics = _getAtomics(this); session[sessionNewDocuments].set(this, initialState); } else { const state = session[sessionNewDocuments].get(this); - for (const path of Object.keys(this.$__.activePaths.states.modify)) { + for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) { state.modifiedPaths.add(path); } state.atomics = _getAtomics(this, state.atomics); diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index ea35736cc33..80d7f55b9dd 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -36,6 +36,13 @@ let Subdocument; */ function DocumentArrayPath(key, schema, options, schemaOptions) { + const schemaTypeIdOption = DocumentArrayPath.defaultOptions && + DocumentArrayPath.defaultOptions._id; + if (schemaTypeIdOption != null) { + schemaOptions = schemaOptions || {}; + schemaOptions._id = schemaTypeIdOption; + } + if (schemaOptions != null && schemaOptions._id != null) { schema = handleIdOption(schema, schemaOptions); } else if (options != null && options._id != null) { @@ -342,7 +349,7 @@ DocumentArrayPath.prototype.doValidateSync = function(array, scope, options) { * ignore */ -DocumentArrayPath.prototype.getDefault = function(scope) { +DocumentArrayPath.prototype.getDefault = function(scope, init, options) { let ret = typeof this.defaultValue === 'function' ? this.defaultValue.call(scope) : this.defaultValue; @@ -351,6 +358,10 @@ DocumentArrayPath.prototype.getDefault = function(scope) { return ret; } + if (options && options.skipCast) { + return ret; + } + // lazy load MongooseDocumentArray || (MongooseDocumentArray = require('../types/DocumentArray')); diff --git a/lib/schematype.js b/lib/schematype.js index 832aa15c4aa..e36b237f240 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -1111,7 +1111,7 @@ SchemaType.prototype.ref = function(ref) { * @api private */ -SchemaType.prototype.getDefault = function(scope, init) { +SchemaType.prototype.getDefault = function(scope, init, options) { let ret; if (typeof this.defaultValue === 'function') { if ( @@ -1132,6 +1132,10 @@ SchemaType.prototype.getDefault = function(scope, init) { ret = utils.clone(ret); } + if (options && options.skipCast) { + return this._applySetters(ret, scope); + } + const casted = this.applySetters(ret, scope, init, undefined, setOptionsForDefaults); if (casted && !Array.isArray(casted) && casted.$isSingleNested) { casted.$__parent = scope; diff --git a/lib/statemachine.js b/lib/statemachine.js index 43da62812e2..a524c999939 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -38,19 +38,12 @@ StateMachine.ctor = function() { StateMachine.apply(this, arguments); this.paths = {}; this.states = {}; - this.stateNames = states; - - let i = states.length, - state; - - while (i--) { - state = states[i]; - this.states[state] = {}; - } }; ctor.prototype = new StateMachine(); + ctor.prototype.stateNames = states; + states.forEach(function(state) { // Changes the `path`'s state to `state`. ctor.prototype[state] = function(path) { @@ -76,6 +69,7 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { if (prevBucket) delete prevBucket[path]; this.paths[path] = nextState; + this.states[nextState] = this.states[nextState] || {}; this.states[nextState][path] = true; }; @@ -84,6 +78,9 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { */ StateMachine.prototype.clear = function clear(state) { + if (this.states[state] == null) { + return; + } const keys = Object.keys(this.states[state]); let i = keys.length; let path; @@ -108,6 +105,17 @@ StateMachine.prototype.clearPath = function clearPath(path) { delete this.states[state][path]; }; +/*! + * Gets the paths for the given state, or empty object `{}` if none. + */ + +StateMachine.prototype.getStatePaths = function getStatePaths(state) { + if (this.states[state] != null) { + return this.states[state]; + } + return {}; +}; + /*! * Checks to see if at least one path is in the states passed in via `arguments` * e.g., this.some('required', 'inited') @@ -120,6 +128,9 @@ StateMachine.prototype.some = function some() { const _this = this; const what = arguments.length ? arguments : this.stateNames; return Array.prototype.some.call(what, function(state) { + if (_this.states[state] == null) { + return false; + } return Object.keys(_this.states[state]).length; }); }; @@ -143,6 +154,9 @@ StateMachine.prototype._iter = function _iter(iterMethod) { const _this = this; const paths = states.reduce(function(paths, state) { + if (_this.states[state] == null) { + return paths; + } return paths.concat(Object.keys(_this.states[state])); }, []); diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index 79e6bd34b51..49cd0c45aad 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -32,15 +32,15 @@ describe('Lean Tutorial', function() { // To enable the `lean` option for a query, use the `lean()` function. const leanDoc = await MyModel.findOne().lean(); - v8Serialize(normalDoc).length; // approximately 300 - v8Serialize(leanDoc).length; // 32, more than 10x smaller! + v8Serialize(normalDoc).length; // approximately 180 + v8Serialize(leanDoc).length; // 32, about 5x smaller! // In case you were wondering, the JSON form of a Mongoose doc is the same // as the POJO. This additional memory only affects how much memory your // Node.js process uses, not how much data is sent over the network. JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true // acquit:ignore:start - assert.ok(v8Serialize(normalDoc).length >= 300 && v8Serialize(normalDoc).length <= 800, v8Serialize(normalDoc).length); + assert.ok(v8Serialize(normalDoc).length >= 150 && v8Serialize(normalDoc).length <= 200, v8Serialize(normalDoc).length); assert.equal(v8Serialize(leanDoc).length, 32); assert.equal(JSON.stringify(normalDoc).length, JSON.stringify(leanDoc).length); // acquit:ignore:end diff --git a/test/model.test.js b/test/model.test.js index f47b864aadd..01585f75b9f 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4346,6 +4346,58 @@ describe('Model', function() { await db.close(); }); + describe('insertMany()', function() { + it('with timestamps (gh-723)', function() { + const schema = new Schema({ name: String }, { timestamps: true }); + const Movie = db.model('Movie', schema); + const start = Date.now(); + + const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + return Movie.insertMany(arr). + then(docs => { + assert.equal(docs.length, 2); + assert.ok(!docs[0].isNew); + assert.ok(!docs[1].isNew); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(docs[1].createdAt.valueOf() >= start); + }). + then(() => Movie.find()). + then(docs => { + assert.equal(docs.length, 2); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(docs[1].createdAt.valueOf() >= start); + }); + }); + + it('insertMany() with nested timestamps (gh-12060)', async function() { + const childSchema = new Schema({ name: { type: String } }, { + _id: false, + timestamps: true + }); + + const parentSchema = new Schema({ child: childSchema }, { + timestamps: true + }); + + const Test = db.model('Test', parentSchema); + + await Test.insertMany([{ child: { name: 'test' } }]); + let docs = await Test.find(); + + assert.equal(docs.length, 1); + assert.equal(docs[0].child.name, 'test'); + assert.ok(docs[0].child.createdAt); + assert.ok(docs[0].child.updatedAt); + + await Test.insertMany([{ child: { name: 'test2' } }], { timestamps: false }); + docs = await Test.find({ 'child.name': 'test2' }); + assert.equal(docs.length, 1); + assert.equal(docs[0].child.name, 'test2'); + assert.ok(!docs[0].child.createdAt); + assert.ok(!docs[0].child.updatedAt); + }); + }); + describe('bug fixes', function() { it('doesnt crash (gh-1920)', function(done) { const parentSchema = new Schema({ @@ -4638,28 +4690,6 @@ describe('Model', function() { }); }); - it('insertMany() with timestamps (gh-723)', function() { - const schema = new Schema({ name: String }, { timestamps: true }); - const Movie = db.model('Movie', schema); - const start = Date.now(); - - const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; - return Movie.insertMany(arr). - then(docs => { - assert.equal(docs.length, 2); - assert.ok(!docs[0].isNew); - assert.ok(!docs[1].isNew); - assert.ok(docs[0].createdAt.valueOf() >= start); - assert.ok(docs[1].createdAt.valueOf() >= start); - }). - then(() => Movie.find()). - then(docs => { - assert.equal(docs.length, 2); - assert.ok(docs[0].createdAt.valueOf() >= start); - assert.ok(docs[1].createdAt.valueOf() >= start); - }); - }); - it('returns empty array if no documents (gh-8130)', function() { const Movie = db.model('Movie', Schema({ name: String })); return Movie.insertMany([]).then(docs => assert.deepEqual(docs, [])); @@ -8611,6 +8641,19 @@ describe('Model', function() { assert.equal(doc.hoursToMake, null); }); + it('supports setters option for `hydrate()` (gh-11653)', function() { + const schema = Schema({ + text: { + type: String, + set: v => v.toLowerCase() + } + }); + const Test = db.model('Test', schema); + + const doc = Test.hydrate({ text: 'FOOBAR' }, null, { setters: true }); + assert.equal(doc.text, 'foobar'); + }); + it('sets index collation based on schema collation (gh-7621)', async function() { let testSchema = new Schema( { name: { type: String, index: true } } @@ -8639,6 +8682,129 @@ describe('Model', function() { assert.deepEqual(indexes[1].key, { name: 1 }); assert.strictEqual(indexes[1].collation.locale, 'en'); }); + + describe('Model.applyDefaults (gh-11945)', function() { + it('applies defaults to POJOs', function() { + const Test = db.model('Test', mongoose.Schema({ + _id: false, + name: { + type: String, + default: 'John Smith' + }, + age: { + type: Number, + default: 29 + }, + nestedName: { + first: { + type: String, + default: 'John' + }, + last: { + type: String, + default: 'Smith' + }, + middle: { + type: String, + default: '' + } + }, + subdoc: { + type: mongoose.Schema({ + _id: false, + test: { + type: String, + default: 'subdoc default' + } + }), + default: () => ({}) + }, + docArr: [{ + _id: false, + test: { + type: String, + default: 'doc array default' + } + }] + })); + + const obj = { age: 31, nestedName: { middle: 'James' }, docArr: [{}] }; + Test.applyDefaults(obj); + + assert.deepStrictEqual(obj, { + name: 'John Smith', + age: 31, + nestedName: { first: 'John', last: 'Smith', middle: 'James' }, + subdoc: { + test: 'subdoc default' + }, + docArr: [{ + test: 'doc array default' + }] + }); + }); + + it('applies defaults to documents', function() { + const Test = db.model('Test', mongoose.Schema({ + _id: false, + name: { + type: String, + default: 'John Smith' + }, + age: { + type: Number, + default: 29 + }, + nestedName: { + first: { + type: String, + default: 'John' + }, + last: { + type: String, + default: 'Smith' + }, + middle: { + type: String, + default: '' + } + }, + subdoc: { + type: mongoose.Schema({ + _id: false, + test: { + type: String, + default: 'subdoc default' + } + }), + default: () => ({}) + }, + docArr: [{ + _id: false, + test: { + type: String, + default: 'doc array default' + } + }] + })); + + const obj = { age: 31, nestedName: { middle: 'James' }, docArr: [{}] }; + const doc = new Test(obj, null, { defaults: false }); + Test.applyDefaults(doc); + + assert.deepStrictEqual(doc.toObject(), { + name: 'John Smith', + age: 31, + nestedName: { first: 'John', last: 'Smith', middle: 'James' }, + subdoc: { + test: 'subdoc default' + }, + docArr: [{ + test: 'doc array default' + }] + }); + }); + }); }); describe('Check if static function that is supplied in schema option is available', function() { diff --git a/test/types/PipelineStage.test.ts b/test/types/PipelineStage.test.ts index 37968a3b366..67dc3078185 100644 --- a/test/types/PipelineStage.test.ts +++ b/test/types/PipelineStage.test.ts @@ -415,3 +415,11 @@ const stages4: PipelineStage[] = [ } } ]; + +(function gh12096() { + const data: PipelineStage.AddFields = { + $addFields: { + name: { $meta: 'Bill' } + } + }; +})(); diff --git a/test/types/expressions.test.ts b/test/types/expressions.test.ts index 00509363c07..be57ddaf38e 100644 --- a/test/types/expressions.test.ts +++ b/test/types/expressions.test.ts @@ -203,3 +203,17 @@ const switchExpr: Expression.Switch = { default: 'Hello' } }; + +(function gh12058() { + const concat: Expression.ConcatArrays = { + $concatArrays: [ + { + $cond: { + if: { $eq: ['foo', true] }, + then: [1], + else: [2] + } + } + ] + }; +})(); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 193992db7fb..46e5920dfbf 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -1,5 +1,14 @@ import { ObjectId } from 'bson'; -import { Schema, Document, Model, connection, model, Types, UpdateQuery, CallbackError } from 'mongoose'; +import { + Schema, + Document, + Model, + connection, + model, + Types, + UpdateQuery, + CallbackError +} from 'mongoose'; import { expectAssignable, expectError, expectType } from 'tsd'; import { AutoTypedSchemaType, autoTypedSchema } from './schema.test'; import { UpdateOneModel } from 'mongodb'; @@ -346,3 +355,11 @@ function gh12100() { Model.syncIndexes({ continueOnError: true, noResponse: true }); Model.syncIndexes({ continueOnError: false, noResponse: true }); } + +(function gh12070() { + const schema_with_string_id = new Schema({ _id: String, nickname: String }); + const TestModel = model('test', schema_with_string_id); + const obj = new TestModel(); + + expectType(obj._id); +})(); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 7a881947f7c..7a6550f7abf 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -325,5 +325,19 @@ function gh11964() { /* ... */ } } +} +function gh12091() { + interface IUser{ + friendsNames: string[]; + } + const userSchema = new Schema({ + friendsNames: [String] + }); + + const update: UpdateQuery = { $addToSet: { friendsNames: 'John Doe' } }; + if (!update?.$addToSet) { + return; + } + update.$addToSet.friendsNames = 'Jane Doe'; } diff --git a/types/expressions.d.ts b/types/expressions.d.ts index 8624b155b6c..bce492cfe5d 100644 --- a/types/expressions.d.ts +++ b/types/expressions.d.ts @@ -1104,7 +1104,7 @@ declare module 'mongoose' { * @version 3.2 * @see https://docs.mongodb.com/manual/reference/operator/aggregation/concatArrays/#mongodb-expression-exp.-concatArrays */ - $concatArrays: ArrayExpression[]; + $concatArrays: Expression[]; } export interface Filter { @@ -2449,7 +2449,7 @@ declare module 'mongoose' { FunctionExpression | ObjectIdExpression | ConditionalExpressionOperator | - Expression.Let; + any; export type ObjectIdExpression = TypeExpressionOperatorReturningObjectId; diff --git a/types/index.d.ts b/types/index.d.ts index 00252c43dd2..3cd944d0d8f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -109,9 +109,7 @@ declare module 'mongoose' { } export type Require_id = T extends { _id?: infer U } - ? U extends any - ? (T & { _id: Types.ObjectId }) - : T & Required<{ _id: U }> + ? IfAny> : T & { _id: Types.ObjectId }; export type RequireOnlyTypedId = T extends { _id?: infer U; } @@ -439,6 +437,10 @@ declare module 'mongoose' { export type SortOrder = -1 | 1 | 'asc' | 'ascending' | 'desc' | 'descending'; + type Mutable = { + -readonly [K in keyof T]: T[K]; + }; + type _UpdateQuery = { /** @see https://docs.mongodb.com/manual/reference/operator/update-field/ */ $currentDate?: AnyKeys & AnyObject; @@ -452,10 +454,10 @@ declare module 'mongoose' { $unset?: AnyKeys & AnyObject; /** @see https://docs.mongodb.com/manual/reference/operator/update-array/ */ - $addToSet?: mongodb.SetFields; + $addToSet?: Mutable>; $pop?: AnyKeys & AnyObject; - $pull?: mongodb.PullOperator; - $push?: mongodb.PushOperator; + $pull?: Mutable>; + $push?: Mutable>; $pullAll?: mongodb.PullAllOperator; /** @see https://docs.mongodb.com/manual/reference/operator/update-bitwise/ */ @@ -539,7 +541,7 @@ declare module 'mongoose' { export type SchemaDefinitionType = T extends Document ? Omit> : T; // Helpers to simplify checks - type IfAny = 0 extends (1 & IFTYPE) ? THENTYPE : IFTYPE; + type IfAny = 0 extends (1 & IFTYPE) ? THENTYPE : ELSETYPE; type IfUnknown = unknown extends IFTYPE ? THENTYPE : IFTYPE; // tests for these two types are located in test/types/lean.test.ts diff --git a/types/models.d.ts b/types/models.d.ts index 6396a4bebf1..9fae6afa66e 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -128,27 +128,27 @@ declare module 'mongoose' { base: Mongoose; /** - * If this is a discriminator model, `baseModelName` is the name of - * the base model. - */ + * If this is a discriminator model, `baseModelName` is the name of + * the base model. + */ baseModelName: string | undefined; /** - * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, - * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one - * command. This is faster than sending multiple independent operations (e.g. - * if you use `create()`) because with `bulkWrite()` there is only one network - * round trip to the MongoDB server. - */ + * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, + * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one + * command. This is faster than sending multiple independent operations (e.g. + * if you use `create()`) because with `bulkWrite()` there is only one network + * round trip to the MongoDB server. + */ bulkWrite(writes: Array, options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions, callback: Callback): void; bulkWrite(writes: Array, callback: Callback): void; bulkWrite(writes: Array, options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions): Promise; /** - * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than - * sending multiple `save()` calls because with `bulkSave()` there is only one - * network round trip to the MongoDB server. - */ + * Sends multiple `save()` calls in a single `bulkWrite()`. This is faster than + * sending multiple `save()` calls because with `bulkSave()` there is only one + * network round trip to the MongoDB server. + */ bulkSave(documents: Array, options?: mongodb.BulkWriteOptions & { timestamps?: boolean }): Promise; /** Collection the model uses. */ @@ -170,10 +170,10 @@ declare module 'mongoose' { create>(doc: T | DocContents, callback: Callback>): void; /** - * Create the collection for this model. By default, if no indexes are specified, - * mongoose will not create the collection for the model until any documents are - * created. Use this method to create the collection explicitly. - */ + * Create the collection for this model. By default, if no indexes are specified, + * mongoose will not create the collection for the model until any documents are + * created. Use this method to create the collection explicitly. + */ createCollection(options: mongodb.CreateCollectionOptions & Pick | null, callback: Callback>): void; createCollection(callback: Callback>): void; createCollection(options?: mongodb.CreateCollectionOptions & Pick): Promise>; @@ -182,34 +182,34 @@ declare module 'mongoose' { db: Connection; /** - * Deletes all of the documents that match `conditions` from the collection. - * Behaves like `remove()`, but deletes all documents that match `conditions` - * regardless of the `single` option. - */ + * Deletes all of the documents that match `conditions` from the collection. + * Behaves like `remove()`, but deletes all documents that match `conditions` + * regardless of the `single` option. + */ deleteMany(filter?: FilterQuery, options?: QueryOptions, callback?: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteMany(filter: FilterQuery, callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteMany(callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; /** - * Deletes the first document that matches `conditions` from the collection. - * Behaves like `remove()`, but deletes at most one document regardless of the - * `single` option. - */ + * Deletes the first document that matches `conditions` from the collection. + * Behaves like `remove()`, but deletes at most one document regardless of the + * `single` option. + */ deleteOne(filter?: FilterQuery, options?: QueryOptions, callback?: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteOne(filter: FilterQuery, callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; deleteOne(callback: CallbackWithoutResult): QueryWithHelpers, TQueryHelpers, T>; /** - * Event emitter that reports any errors that occurred. Useful for global error - * handling. - */ + * Event emitter that reports any errors that occurred. Useful for global error + * handling. + */ events: NodeJS.EventEmitter; /** - * Finds a single document by its _id field. `findById(id)` is almost* - * equivalent to `findOne({ _id: id })`. If you want to query by a document's - * `_id`, use `findById()` instead of `findOne()`. - */ + * Finds a single document by its _id field. `findById(id)` is almost* + * equivalent to `findOne({ _id: id })`. If you want to query by a document's + * `_id`, use `findById()` instead of `findOne()`. + */ findById>( id: any, projection?: ProjectionType | null, @@ -240,19 +240,19 @@ declare module 'mongoose' { ): QueryWithHelpers; /** - * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. - * The document returned has no paths marked as modified initially. - */ - hydrate(obj: any): HydratedDocument; + * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. + * The document returned has no paths marked as modified initially. + */ + hydrate(obj: any, projection?: AnyObject, options?: { setters?: boolean }): HydratedDocument; /** - * This function is responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), - * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. - * Mongoose calls this function automatically when a model is created using - * [`mongoose.model()`](/docs/api.html#mongoose_Mongoose-model) or - * [`connection.model()`](/docs/api.html#connection_Connection-model), so you - * don't need to call it. - */ + * This function is responsible for building [indexes](https://docs.mongodb.com/manual/indexes/), + * unless [`autoIndex`](http://mongoosejs.com/docs/guide.html#autoIndex) is turned off. + * Mongoose calls this function automatically when a model is created using + * [`mongoose.model()`](/docs/api.html#mongoose_Mongoose-model) or + * [`connection.model()`](/docs/api.html#connection_Connection-model), so you + * don't need to call it. + */ init(callback?: CallbackWithoutResult): Promise>; /** Inserts one or more new documents as a single `insertMany` call to the MongoDB server. */