diff --git a/lib/document.js b/lib/document.js index 495f8665c64..aa7fb04be26 100644 --- a/lib/document.js +++ b/lib/document.js @@ -18,6 +18,7 @@ const ValidatorError = require('./error/validator'); const VirtualType = require('./virtualtype'); const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren'); const promiseOrCallback = require('./helpers/promiseOrCallback'); +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; @@ -153,7 +154,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) { @@ -177,7 +178,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) { @@ -451,122 +452,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 */ @@ -786,7 +671,8 @@ Document.prototype.$__init = function(doc, opts) { const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ? $__hasIncludedChildren(this.$__.selected) : null; - $__applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); + + applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); return this; }; 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/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/model.js b/lib/model.js index 26fb9a511a0..bb79be729b6 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'); @@ -96,7 +98,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. @@ -210,6 +212,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 @@ -888,26 +891,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; /** @@ -937,22 +944,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). @@ -995,6 +992,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 * @@ -1104,6 +1102,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' }); * @@ -1273,7 +1272,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); @@ -1491,7 +1490,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. */ @@ -1655,7 +1654,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) { @@ -1890,6 +1889,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 @@ -2386,7 +2386,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)); @@ -2422,7 +2422,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: * @@ -2476,19 +2476,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 @@ -2534,6 +2521,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 @@ -2614,17 +2608,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 @@ -2654,11 +2637,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] @@ -2669,6 +2650,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 @@ -2717,15 +2705,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 @@ -2740,17 +2719,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 @@ -2830,15 +2811,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 @@ -2856,6 +2828,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 @@ -2909,15 +2885,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 @@ -2932,17 +2899,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 @@ -2989,13 +2958,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 @@ -3009,6 +2971,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 @@ -3396,7 +3361,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); @@ -3692,6 +3657,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()`. * @@ -3702,6 +3691,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`); @@ -3862,7 +3852,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 @@ -4028,54 +4017,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); @@ -4084,8 +4053,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; @@ -4095,14 +4064,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); @@ -4115,20 +4098,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/schema/documentarray.js b/lib/schema/documentarray.js index f44ccf9a9d3..80d7f55b9dd 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -349,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; @@ -358,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/test/model.test.js b/test/model.test.js index f47b864aadd..d4ab16ad176 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, [])); @@ -8639,6 +8669,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