diff --git a/index.js b/index.js index 01a9c13912c..4f4fa8ebe03 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,7 @@ module.exports.Mixed = mongoose.Mixed; module.exports.Date = mongoose.Date; module.exports.Number = mongoose.Number; module.exports.Error = mongoose.Error; +module.exports.MongooseError = mongoose.MongooseError; module.exports.now = mongoose.now; module.exports.CastError = mongoose.CastError; module.exports.SchemaTypeOptions = mongoose.SchemaTypeOptions; diff --git a/lib/document.js b/lib/document.js index 450e286914d..81d51a2ce3e 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3676,6 +3676,15 @@ Document.prototype.$toObject = function(options, json) { flattenMaps = schemaOptions.flattenMaps; } + let flattenObjectIds; + if (options._calledWithOptions.flattenObjectIds != null) { + flattenObjectIds = options.flattenObjectIds; + } else if (defaultOptions.flattenObjectIds != null) { + flattenObjectIds = defaultOptions.flattenObjectIds; + } else { + flattenObjectIds = schemaOptions.flattenObjectIds; + } + // The original options that will be passed to `clone()`. Important because // `clone()` will recursively call `$toObject()` on embedded docs, so we // need the original options the user passed in, plus `_isNested` and @@ -3685,6 +3694,7 @@ Document.prototype.$toObject = function(options, json) { json: json, minimize: _minimize, flattenMaps: flattenMaps, + flattenObjectIds: flattenObjectIds, _seen: (options && options._seen) || new Map() }); @@ -3900,6 +3910,7 @@ Document.prototype.$toObject = function(options, json) { * @param {Boolean} [options.depopulate=false] if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths. * @param {Boolean} [options.versionKey=true] if false, exclude the version key (`__v` by default) from the output * @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`. + * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings. * @param {Boolean} [options.useProjection=false] - If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema. * @return {Object} js object (not a POJO) * @see mongodb.Binary https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html @@ -3968,8 +3979,7 @@ function applyVirtuals(self, json, options, toObjectOptions) { let virtualsToApply = null; if (Array.isArray(options.virtuals)) { virtualsToApply = new Set(options.virtuals); - } - else if (options.virtuals && options.virtuals.pathsToSkip) { + } else if (options.virtuals && options.virtuals.pathsToSkip) { virtualsToApply = new Set(paths); for (let i = 0; i < options.virtuals.pathsToSkip.length; i++) { if (virtualsToApply.has(options.virtuals.pathsToSkip[i])) { @@ -4181,6 +4191,7 @@ function omitDeselectedFields(self, json) { * * @param {Object} options * @param {Boolean} [options.flattenMaps=true] if true, convert Maps to [POJOs](https://masteringjs.io/tutorials/fundamentals/pojo). Useful if you want to `JSON.stringify()` the result. + * @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings. * @return {Object} * @see Document#toObject https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject() * @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html @@ -4193,6 +4204,9 @@ Document.prototype.toJSON = function(options) { return this.$toObject(options, true); }; +/*! + * ignore + */ Document.prototype.ownerDocument = function() { return this; diff --git a/lib/error/bulkWriteError.js b/lib/error/bulkWriteError.js new file mode 100644 index 00000000000..1711b03b586 --- /dev/null +++ b/lib/error/bulkWriteError.js @@ -0,0 +1,41 @@ +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseError = require('./'); + + +/** + * If `bulkWrite()` or `insertMany()` has validation errors, but + * all valid operations succeed, and 'throwOnValidationError' is true, + * Mongoose will throw this error. + * + * @api private + */ + +class MongooseBulkWriteError extends MongooseError { + constructor(validationErrors, results, rawResult, operation) { + let preview = validationErrors.map(e => e.message).join(', '); + if (preview.length > 200) { + preview = preview.slice(0, 200) + '...'; + } + super(`${operation} failed with ${validationErrors.length} Mongoose validation errors: ${preview}`); + + this.validationErrors = validationErrors; + this.results = results; + this.rawResult = rawResult; + this.operation = operation; + } +} + +Object.defineProperty(MongooseBulkWriteError.prototype, 'name', { + value: 'MongooseBulkWriteError' +}); + +/*! + * exports + */ + +module.exports = MongooseBulkWriteError; diff --git a/lib/error/invalidSchemaOption.js b/lib/error/invalidSchemaOption.js new file mode 100644 index 00000000000..2ab1aa9497e --- /dev/null +++ b/lib/error/invalidSchemaOption.js @@ -0,0 +1,30 @@ + +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseError = require('./'); + +class InvalidSchemaOptionError extends MongooseError { + /** + * InvalidSchemaOption Error constructor. + * @param {String} name + * @api private + */ + constructor(name, option) { + const msg = `Cannot create use schema for property "${name}" because the schema has the ${option} option enabled.`; + super(msg); + } +} + +Object.defineProperty(InvalidSchemaOptionError.prototype, 'name', { + value: 'InvalidSchemaOptionError' +}); + +/*! + * exports + */ + +module.exports = InvalidSchemaOptionError; diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 983348d5635..8aac7163422 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -78,6 +78,9 @@ function clone(obj, options, isArrayChild) { } if (isBsonType(obj, 'ObjectId')) { + if (options && options.flattenObjectIds) { + return obj.toJSON(); + } return new ObjectId(obj.id); } diff --git a/lib/index.js b/lib/index.js index c838b3ea2b5..aa8fe59e979 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1140,6 +1140,7 @@ Mongoose.prototype.Number = SchemaTypes.Number; */ Mongoose.prototype.Error = require('./error/index'); +Mongoose.prototype.MongooseError = require('./error/mongooseError'); /** * Mongoose uses this function to get the current time when setting diff --git a/lib/model.js b/lib/model.js index 361301bdb38..2f5d3a6ec3a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -64,6 +64,7 @@ const setDottedPath = require('./helpers/path/setDottedPath'); const STATES = require('./connectionstate'); const util = require('util'); const utils = require('./utils'); +const MongooseBulkWriteError = require('./error/bulkWriteError'); const VERSION_WHERE = 1; const VERSION_INC = 2; @@ -1855,20 +1856,24 @@ Model.discriminators; * * #### Example: * - * Character - * .find(Character.translateAliases({ - * '名': 'Eddard Stark' // Alias for 'name' - * }) - * .exec(function(err, characters) {}) + * await Character.find(Character.translateAliases({ + * '名': 'Eddard Stark' // Alias for 'name' + * }); + * + * By default, `translateAliases()` overwrites raw fields with aliased fields. + * So if `n` is an alias for `name`, `{ n: 'alias', name: 'raw' }` will resolve to `{ name: 'alias' }`. + * However, you can set the `errorOnDuplicates` option to throw an error if there are potentially conflicting paths. + * The `translateAliases` option for queries uses `errorOnDuplicates`. * * #### Note: * * Only translate arguments of object type anything else is returned raw * * @param {Object} fields fields/conditions that may contain aliased keys + * @param {Boolean} [errorOnDuplicates] if true, throw an error if there's both a key and an alias for that key in `fields` * @return {Object} the translated 'pure' fields/conditions */ -Model.translateAliases = function translateAliases(fields) { +Model.translateAliases = function translateAliases(fields, errorOnDuplicates) { _checkContext(this, 'translateAliases'); const translate = (key, value) => { @@ -1880,6 +1885,9 @@ Model.translateAliases = function translateAliases(fields) { const name = fieldKeys[i]; if (currentSchema && currentSchema.aliases[name]) { alias = currentSchema.aliases[name]; + if (errorOnDuplicates && alias in fields) { + throw new MongooseError(`Provided object has both field "${name}" and its alias "${alias}"`); + } // Alias found, translated.push(alias); } else { @@ -1932,6 +1940,8 @@ Model.translateAliases = function translateAliases(fields) { // Recursively translate nested queries fields[key][i] = this.translateAliases(fields[key][i]); } + } else { + this.translateAliases(fields[key]); } } } @@ -1961,6 +1971,7 @@ Model.translateAliases = function translateAliases(fields) { * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ @@ -1995,6 +2006,7 @@ Model.deleteOne = function deleteOne(conditions, options) { * * @param {Object} conditions * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ @@ -2036,6 +2048,7 @@ Model.deleteMany = function deleteMany(conditions, options) { * @param {Object|ObjectId} filter * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select() * @see query casting https://mongoosejs.com/docs/tutorials/query_casting.html @@ -2124,6 +2137,7 @@ Model.findById = function findById(id, projection, options) { * @param {Object} [conditions] * @param {Object|String|String[]} [projection] optional fields to return, see [`Query.prototype.select()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.select()) * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see field selection https://mongoosejs.com/docs/api/query.html#Query.prototype.select() * @see lean queries https://mongoosejs.com/docs/tutorials/lean.html @@ -2317,7 +2331,7 @@ Model.$where = function $where() { }; /** - * Issues a mongodb findAndModify update command. + * Issues a mongodb findOneAndUpdate command. * * 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. * @@ -2369,6 +2383,7 @@ Model.$where = function $where() { * @param {Boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/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.9/interfaces/ModifyResult.html) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2429,7 +2444,7 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) { } /** - * Issues a mongodb findAndModify update command by a document's _id field. + * Issues a mongodb findOneAndUpdate command by a document's _id field. * `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`. * * Finds a matching document, updates it according to the `update` arg, @@ -2487,6 +2502,7 @@ function _decorateUpdateWithVersionKey(update, options, versionKey) { * @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 {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2547,6 +2563,7 @@ Model.findByIdAndUpdate = function(id, update, options) { * @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 {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ @@ -2582,6 +2599,7 @@ Model.findOneAndDelete = function(conditions, options) { * @param {Object|Number|String} id value of `_id` to query by * @param {Object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndRemove https://mongoosejs.com/docs/api/model.html#Model.findOneAndRemove() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2625,6 +2643,7 @@ Model.findByIdAndDelete = function(id, options) { * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/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 {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @api public */ @@ -2649,7 +2668,7 @@ Model.findOneAndReplace = function(filter, replacement, options) { }; /** - * Issue a mongodb findAndModify remove command. + * Issue a mongodb findOneAndRemove command. * * Finds a matching document, removes it, and returns the found document (if any). * @@ -2682,6 +2701,7 @@ Model.findOneAndReplace = function(filter, replacement, options) { * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/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 {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @api public @@ -2707,7 +2727,7 @@ Model.findOneAndRemove = function(conditions, options) { }; /** - * Issue a mongodb findAndModify remove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`. + * Issue a mongodb findOneAndRemove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`. * * Finds a matching document, removes it, and returns the found document (if any). * @@ -2729,6 +2749,7 @@ Model.findOneAndRemove = function(conditions, options) { * @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.9/interfaces/ModifyResult.html) * @param {Object|String} [options.select] sets the document fields to return. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Model.findOneAndRemove https://mongoosejs.com/docs/api/model.html#Model.findOneAndRemove() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2962,6 +2983,7 @@ Model.startSession = function() { * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting. * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @return {Promise} resolving to the raw result from the MongoDB driver if `options.rawResult` was `true`, or the documents that passed validation, otherwise * @api public */ @@ -3007,6 +3029,7 @@ Model.$__insertMany = function(arr, options, callback) { const limit = options.limit || 1000; const rawResult = !!options.rawResult; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; + const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; if (!Array.isArray(arr)) { @@ -3113,6 +3136,20 @@ Model.$__insertMany = function(arr, options, callback) { _setIsNew(attribute, false); } + if (ordered === false && throwOnValidationError && validationErrors.length > 0) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; + } + } + return callback(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'insertMany' + )); + } + if (rawResult) { if (ordered === false) { for (let i = 0; i < results.length; ++i) { @@ -3308,6 +3345,7 @@ function _setIsNew(doc, val) { * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) * @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default. * @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](https://mongoosejs.com/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. * @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds * @api public @@ -3355,12 +3393,14 @@ Model.bulkWrite = async function bulkWrite(ops, options) { let remaining = validations.length; let validOps = []; let validationErrors = []; + const results = []; for (let i = 0; i < validations.length; ++i) { validations[i]((err) => { if (err == null) { validOps.push(i); } else { validationErrors.push({ index: i, error: err }); + results[i] = err; } if (--remaining <= 0) { completeUnorderedValidation.call(this); @@ -3373,6 +3413,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { map(v => v.error); function completeUnorderedValidation() { + const validOpIndexes = validOps; validOps = validOps.sort().map(index => ops[index]); this.$__collection.bulkWrite(validOps, options, (error, res) => { @@ -3385,9 +3426,22 @@ Model.bulkWrite = async function bulkWrite(ops, options) { return reject(error); } + for (let i = 0; i < validOpIndexes.length; ++i) { + results[validOpIndexes[i]] = null; + } if (validationErrors.length > 0) { - res.mongoose = res.mongoose || {}; - res.mongoose.validationErrors = validationErrors; + if (options.throwOnValidationError) { + return reject(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'bulkWrite' + )); + } else { + res.mongoose = res.mongoose || {}; + res.mongoose.validationErrors = validationErrors; + res.mongoose.results = results; + } } resolve(res); @@ -3739,6 +3793,7 @@ Model.hydrate = function(obj, projection, options) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output @@ -3777,6 +3832,7 @@ Model.updateMany = function updateMany(conditions, doc, options) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/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.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output @@ -3813,6 +3869,7 @@ Model.updateOne = function updateOne(conditions, doc, options) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see UpdateResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/UpdateResult.html diff --git a/lib/query.js b/lib/query.js index 6e210e05b46..e7e7f046cbb 100644 --- a/lib/query.js +++ b/lib/query.js @@ -154,6 +154,18 @@ Query.prototype = new mquery(); Query.prototype.constructor = Query; Query.base = mquery.prototype; +/*! + * Overwrite mquery's `_distinct`, because Mongoose uses that name + * to store the field to apply distinct on. + */ + +Object.defineProperty(Query.prototype, '_distinct', { + configurable: true, + writable: true, + enumerable: true, + value: undefined +}); + /** * Flag to opt out of using `$geoWithin`. * @@ -1618,6 +1630,11 @@ Query.prototype.setOptions = function(options, overwrite) { this._mongooseOptions.defaults = options.defaults; // deleting options.defaults will cause 7287 to fail } + if ('translateAliases' in options) { + this._mongooseOptions.translateAliases = options.translateAliases; + delete options.translateAliases; + } + if (options.lean == null && this.schema && 'lean' in this.schema.options) { this._mongooseOptions.lean = this.schema.options.lean; } @@ -2237,6 +2254,9 @@ Query.prototype._find = async function _find() { }); const options = this._optionsForExec(); + + this._applyTranslateAliases(options); + const filter = this._conditions; const fields = options.projection; @@ -2486,6 +2506,8 @@ Query.prototype._findOne = async function _findOne() { const options = this._optionsForExec(); + this._applyTranslateAliases(options); + // don't pass in the conditions because we already merged them in const doc = await this._collection.collection.findOne(this._conditions, options); return new Promise((resolve, reject) => { @@ -2520,6 +2542,7 @@ Query.prototype._findOne = async function _findOne() { * @param {Object} [filter] mongodb selector * @param {Object} [projection] optional fields to return * @param {Object} [options] see [`setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()) + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} this * @see findOne https://www.mongodb.com/docs/manual/reference/method/db.collection.findOne/ * @see Query.select https://mongoosejs.com/docs/api/query.html#Query.prototype.select() @@ -2582,9 +2605,12 @@ Query.prototype._count = async function _count() { applyGlobalMaxTimeMS(this.options, this.model); applyGlobalDiskUse(this.options, this.model); - const conds = this._conditions; const options = this._optionsForExec(); + this._applyTranslateAliases(options); + + const conds = this._conditions; + return this._collection.collection.count(conds, options); }; @@ -2609,12 +2635,43 @@ Query.prototype._countDocuments = async function _countDocuments() { applyGlobalMaxTimeMS(this.options, this.model); applyGlobalDiskUse(this.options, this.model); - const conds = this._conditions; const options = this._optionsForExec(); + this._applyTranslateAliases(options); + + const conds = this._conditions; + return this._collection.collection.countDocuments(conds, options); }; +/*! + * If `translateAliases` option is set, call `Model.translateAliases()` + * on the following query properties: filter, projection, update, distinct. + */ + +Query.prototype._applyTranslateAliases = function _applyTranslateAliases(options) { + let applyTranslateAliases = false; + if ('translateAliases' in this._mongooseOptions) { + applyTranslateAliases = this._mongooseOptions.translateAliases; + } else if (this.model?.schema?._userProvidedOptions?.translateAliases != null) { + applyTranslateAliases = this.model.schema._userProvidedOptions.translateAliases; + } else if (this.model?.base?.options?.translateAliases != null) { + applyTranslateAliases = this.model.base.options.translateAliases; + } + if (!applyTranslateAliases) { + return; + } + + if (this.model?.schema?.aliases && Object.keys(this.model.schema.aliases).length > 0) { + this.model.translateAliases(this._conditions, true); + this.model.translateAliases(options.projection, true); + this.model.translateAliases(this._update, true); + if (this._distinct != null && this.model.schema.aliases[this._distinct] != null) { + this._distinct = this.model.schema.aliases[this._distinct]; + } + } +}; + /** * Execute a estimatedDocumentCount() query * @@ -2796,6 +2853,7 @@ Query.prototype.__distinct = async function __distinct() { applyGlobalDiskUse(this.options, this.model); const options = this._optionsForExec(); + this._applyTranslateAliases(options); return this._collection.collection. distinct(this._distinct, this._conditions, options); @@ -2955,6 +3013,7 @@ Query.prototype._deleteOne = async function _deleteOne() { } const options = this._optionsForExec(); + this._applyTranslateAliases(options); return this._collection.collection.deleteOne(this._conditions, options); }; @@ -3032,6 +3091,7 @@ Query.prototype._deleteMany = async function _deleteMany() { } const options = this._optionsForExec(); + this._applyTranslateAliases(options); return this._collection.collection.deleteMany(this._conditions, options); }; @@ -3143,6 +3203,7 @@ function prepareDiscriminatorCriteria(query) { * @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](https://mongoosejs.com/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.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html @@ -3234,6 +3295,7 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() { applyGlobalDiskUse(this.options, this.model); const opts = this._optionsForExec(this.model); + this._applyTranslateAliases(opts); if ('strict' in opts) { this._mongooseOptions.strict = opts.strict; @@ -3427,6 +3489,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() { const filter = this._conditions; const options = this._optionsForExec(); + this._applyTranslateAliases(options); let fields = null; this._applyPaths(); @@ -3496,6 +3559,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() { * @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](https://mongoosejs.com/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.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @return {Query} this * @api public */ @@ -3560,6 +3624,7 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() { const filter = this._conditions; const options = this._optionsForExec(); + this._applyTranslateAliases(options); convertNewToReturnDocument(options); const runValidators = _getOption(this, 'runValidators', false); @@ -3787,6 +3852,7 @@ async function _updateThunk(op) { const castedQuery = this._conditions; const options = this._optionsForExec(this.model); + this._applyTranslateAliases(options); this._update = clone(this._update, options); const isOverwriting = this._mongooseOptions.overwrite && !hasDollarKeys(this._update); @@ -3927,6 +3993,7 @@ Query.prototype._replaceOne = async function _replaceOne() { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() @@ -3995,6 +4062,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/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.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() @@ -4061,6 +4129,7 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() diff --git a/lib/schema.js b/lib/schema.js index 36ad801fa49..e9cb24b0f54 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -744,6 +744,12 @@ Schema.prototype.add = function add(obj, prefix) { if (this._userProvidedOptions.strict != null) { childSchemaOptions.strict = this._userProvidedOptions.strict; } + if (this._userProvidedOptions.toObject != null) { + childSchemaOptions.toObject = this._userProvidedOptions.toObject; + } + if (this._userProvidedOptions.toJSON != null) { + childSchemaOptions.toJSON = this._userProvidedOptions.toJSON; + } const _schema = new Schema(_typeDef, childSchemaOptions); _schema.$implicitlyCreated = true; @@ -1328,6 +1334,12 @@ Schema.prototype.interpretAsType = function(path, obj, options) { if (options.hasOwnProperty('strictQuery')) { childSchemaOptions.strictQuery = options.strictQuery; } + if (options.hasOwnProperty('toObject')) { + childSchemaOptions.toObject = options.toObject; + } + if (options.hasOwnProperty('toJSON')) { + childSchemaOptions.toJSON = options.toJSON; + } if (this._userProvidedOptions.hasOwnProperty('_id')) { childSchemaOptions._id = this._userProvidedOptions._id; diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index c493863980f..6b2058cb3d4 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -18,6 +18,7 @@ const getConstructor = require('../helpers/discriminator/getConstructor'); const handleIdOption = require('../helpers/schema/handleIdOption'); const internalToObjectOptions = require('../options').internalToObjectOptions; const utils = require('../utils'); +const InvalidSchemaOptionError = require('../error/invalidSchemaOption'); let Subdocument; @@ -34,6 +35,9 @@ module.exports = SubdocumentPath; */ function SubdocumentPath(schema, path, options) { + if (schema.options.timeseries) { + throw new InvalidSchemaOptionError(path, 'timeseries'); + } const schemaTypeIdOption = SubdocumentPath.defaultOptions && SubdocumentPath.defaultOptions._id; if (schemaTypeIdOption != null) { diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index c33021b5559..2ba0905ebc5 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -16,6 +16,7 @@ const handleIdOption = require('../helpers/schema/handleIdOption'); const handleSpreadDoc = require('../helpers/document/handleSpreadDoc'); const utils = require('../utils'); const getConstructor = require('../helpers/discriminator/getConstructor'); +const InvalidSchemaOptionError = require('../error/invalidSchemaOption'); const arrayAtomicsSymbol = require('../helpers/symbols').arrayAtomicsSymbol; const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol; @@ -36,6 +37,9 @@ let Subdocument; */ function DocumentArrayPath(key, schema, options, schemaOptions) { + if (schema.options.timeseries) { + throw new InvalidSchemaOptionError(key, 'timeseries'); + } const schemaTypeIdOption = DocumentArrayPath.defaultOptions && DocumentArrayPath.defaultOptions._id; if (schemaTypeIdOption != null) { diff --git a/lib/validoptions.js b/lib/validoptions.js index a42e552c7af..af4e116deec 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -30,7 +30,8 @@ const VALID_OPTIONS = Object.freeze([ 'strictPopulate', 'strictQuery', 'toJSON', - 'toObject' + 'toObject', + 'translateAliases' ]); module.exports = VALID_OPTIONS; diff --git a/package.json b/package.json index 496042b6109..d866b65da5a 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ ], "license": "MIT", "dependencies": { - "bson": "^5.2.0", + "bson": "^5.3.0", "kareem": "2.5.1", - "mongodb": "5.3.0", + "mongodb": "5.5.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -118,7 +118,7 @@ "main": "./index.js", "types": "./types/index.d.ts", "engines": { - "node": ">=14.0.0" + "node": ">=14.20.1" }, "bugs": { "url": "https://github.com/Automattic/mongoose/issues/new" diff --git a/test/document.test.js b/test/document.test.js index b09e2baf7d9..19c9b711a07 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -792,6 +792,24 @@ describe('document', function() { assert.strictEqual(myModel.toObject().foo, void 0); }); + + it('should propogate toObject to implicitly created schemas gh-13325', async function() { + const userSchema = Schema({ + firstName: String, + company: { + type: { companyId: { type: Schema.Types.ObjectId }, companyName: String } + } + }, { + toObject: { virtuals: true } + }); + + userSchema.virtual('company.details').get(() => 42); + + const User = db.model('User', userSchema); + const user = new User({ firstName: 'test', company: { companyName: 'foo' } }); + const obj = user.toObject(); + assert.strictEqual(obj.company.details, 42); + }); }); describe('toJSON', function() { @@ -978,6 +996,27 @@ describe('document', function() { assert.equal(foundAlicJson.friends, undefined); assert.equal(foundAlicJson.name, 'Alic'); }); + it('should propogate toJSON to implicitly created schemas gh-13325', async function() { + const userSchema = Schema({ + firstName: String, + company: { + type: { companyId: { type: Schema.Types.ObjectId }, companyName: String } + } + }, { + id: false, + toJSON: { virtuals: true } + }); + + userSchema.virtual('company.details').get(() => 'foo'); + + const User = db.model('User', userSchema); + const doc = new User({ + firstName: 'test', + company: { companyName: 'Acme Inc' } + }); + const obj = doc.toJSON(); + assert.strictEqual(obj.company.details, 'foo'); + }); }); describe('inspect', function() { @@ -6721,6 +6760,40 @@ describe('document', function() { assert.equal(mapTest.toObject({}).test.key1.name, 'value1'); }); + it('flattenObjectIds option for toObject() (gh-13341) (gh-2790)', function() { + const schema = new Schema({ + _id: 'ObjectId', + nested: { + id: 'ObjectId' + }, + subdocument: new Schema({}), + documentArray: [new Schema({})] + }, { versionKey: false }); + + const Test = db.model('Test', schema); + + const doc = new Test({ + _id: new mongoose.Types.ObjectId('0'.repeat(24)), + nested: { + id: new mongoose.Types.ObjectId('1'.repeat(24)) + }, + subdocument: { + _id: new mongoose.Types.ObjectId('2'.repeat(24)) + }, + documentArray: [{ _id: new mongoose.Types.ObjectId('3'.repeat(24)) }] + }); + assert.deepStrictEqual(doc.toObject({ flattenObjectIds: true }), { + _id: '0'.repeat(24), + nested: { + id: '1'.repeat(24) + }, + subdocument: { + _id: '2'.repeat(24) + }, + documentArray: [{ _id: '3'.repeat(24) }] + }); + }); + it('`collection` property with strict: false (gh-7276)', async function() { const schema = new Schema({}, { strict: false, versionKey: false }); const Model = db.model('Test', schema); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index b5257b35895..452561f8f2a 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -10308,6 +10308,51 @@ describe('model: populate:', function() { assert.ok(err.message.indexOf('l1.l22') !== -1, err.message); }); + it('propagates toObject options to populate virtuals (gh-13325)', async function() { + const userSchema = Schema({ + firstName: String, + companies: { + type: [{ companyId: { type: Schema.Types.ObjectId }, companyName: String }] + } + }, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + + userSchema.virtual('companies.details', { + ref: 'company', + localField: 'companies.companyId', + foreignField: '_id', + justOne: true + }); + + const User = db.model('User', userSchema); + const companySchema = Schema({ + name: { + type: String + }, + legalName: { + type: String, + required: true + } + }); + const Company = db.model('company', companySchema); + + const comp = await Company.create({ + name: 'Google', + legalName: 'Alphabet Inc' + }); + await User.create({ + firstName: 'Test', + companies: [{ companyId: comp._id, companyName: 'Google' }] + }); + const doc = await User.findOne().populate('companies.details'); + let obj = doc.toObject(); + assert.equal(obj.companies[0].details.name, 'Google'); + obj = doc.toJSON(); + assert.equal(obj.companies[0].details.name, 'Google'); + }); + it('respects strictPopulate schema option (gh-11290)', async function() { const kittySchema = Schema({ name: String }, { strictPopulate: false }); diff --git a/test/model.test.js b/test/model.test.js index c6e0648c9a0..d448edae8fc 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4189,6 +4189,45 @@ describe('Model', function() { const { num } = await Test.findById(_id); assert.equal(num, 99); }); + + it('bulkWrite should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ age: { type: Number } }); + const User = db.model('User', userSchema); + + const createdUser = await User.create({ name: 'Test' }); + + const err = await User.bulkWrite([ + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 'NaN' } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 13 } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 12 } }, + upsert: true + } + } + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].path, 'age'); + assert.equal(err.results[0].path, 'age'); + }); }); it('deleteOne with cast error (gh-5323)', async function() { @@ -6177,6 +6216,32 @@ describe('Model', function() { }); + it('insertMany should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ + age: { type: Number } + }); + + const User = db.model('User', userSchema); + + const err = await User.insertMany([ + new User({ age: 12 }), + new User({ age: 12 }), + new User({ age: 'NaN' }) + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['age'].name, 'CastError'); + assert.ok(err.results[2] instanceof Error); + assert.equal(err.results[2].errors['age'].name, 'CastError'); + + const docs = await User.find(); + assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); + }); + it('returns writeResult on success', async() => { const userSchema = new Schema({ diff --git a/test/query.test.js b/test/query.test.js index 760af5abce8..24ef58598e8 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -3666,6 +3666,99 @@ describe('Query', function() { }); + it('translateAliases option (gh-7511)', async function() { + const testSchema = new Schema({ + name: { + type: String, + alias: 'n' + }, + age: { + type: Number + } + }); + const Test = db.model('Test', testSchema); + await Test.create({ name: 'foo', age: 99 }); + + let res = await Test.findOne({ n: 'foo' }, { n: 1 }, { translateAliases: true }); + assert.equal(res.name, 'foo'); + assert.strictEqual(res.age, void 0); + + res = await Test.find({ n: 'foo' }, { n: 1 }, { translateAliases: true }); + assert.equal(res.length, 1); + assert.equal(res[0].name, 'foo'); + assert.strictEqual(res[0].age, void 0); + + res = await Test.countDocuments({ n: 'foo' }, { translateAliases: true }); + assert.strictEqual(res, 1); + + res = await Test.distinct('n').setOptions({ translateAliases: true }); + assert.deepStrictEqual(res, ['foo']); + + res = await Test.findOneAndUpdate( + { n: 'foo' }, + { n: 'bar' }, + { returnDocument: 'after', translateAliases: true } + ); + assert.strictEqual(res.name, 'bar'); + + res = await Test.updateOne( + { n: 'bar' }, + { $set: { age: 44 }, n: 'baz' }, + { translateAliases: true } + ); + assert.strictEqual(res.modifiedCount, 1); + + res = await Test.updateOne( + { name: 'baz' }, + { $set: { n: 'qux' } }, + { translateAliases: true } + ); + assert.strictEqual(res.modifiedCount, 1); + + res = await Test.deleteMany({ n: 'qux' }, { translateAliases: true }); + assert.deepStrictEqual(res.deletedCount, 1); + }); + + it('translateAliases throws error on conflicting properties (gh-7511)', async function() { + const testSchema = new Schema({ + name: { + type: String, + alias: 'n' + }, + age: { + type: Number + } + }); + const Test = db.model('Test', testSchema); + await Test.create({ name: 'foo', age: 99 }); + + await assert.rejects(async() => { + await Test.findOne( + { name: 'foo', n: 'bar' }, + null, + { translateAliases: true } + ); + }, /Provided object has both field "n" and its alias "name"/); + }); + + it('schema level translateAliases option (gh-7511)', async function() { + const testSchema = new Schema({ + name: { + type: String, + alias: 'n' + }, + age: { + type: Number + } + }, { translateAliases: true }); + const Test = db.model('Test', testSchema); + await Test.deleteMany({}); + await Test.create({ name: 'foo', age: 99 }); + + const res = await Test.findOne({ n: 'foo' }); + assert.equal(res.name, 'foo'); + }); + describe('set()', function() { it('overwrites top-level keys if setting to undefined (gh-12155)', function() { const testSchema = new mongoose.Schema({ diff --git a/test/schema.test.js b/test/schema.test.js index ab636acfbd6..af085121c59 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3034,6 +3034,23 @@ describe('schema', function() { schema.removeVirtual('foo'); }, { message: 'Attempting to remove virtual "foo" that does not exist.' }); }); + it('should throw an error if using schema with "timeseries" option as a nested schema', function() { + const subSchema = new Schema({ + name: String + }, { timeseries: { timeField: 'timestamp', metaField: 'metadata', granularity: 'hours' } }); + assert.throws(() => { + new Schema({ + name: String, + array: [subSchema] + }); + }, { message: 'Cannot create use schema for property "array" because the schema has the timeseries option enabled.' }); + assert.throws(() => { + new Schema({ + name: String, + subdoc: subSchema + }); + }, { message: 'Cannot create use schema for property "subdoc" because the schema has the timeseries option enabled.' }); + }); it('should allow timestamps on a sub document when having _id field in the main document gh-13343', async function() { const ImageSchema = new Schema({ dimensions: { diff --git a/types/error.d.ts b/types/error.d.ts index 1b76d8fddeb..226fad31931 100644 --- a/types/error.d.ts +++ b/types/error.d.ts @@ -6,7 +6,7 @@ declare module 'mongoose' { type CastError = Error.CastError; type SyncIndexesError = Error.SyncIndexesError; - class MongooseError extends global.Error { + export class MongooseError extends global.Error { constructor(msg: string); /** The type of error. "MongooseError" for generic errors. */ diff --git a/types/index.d.ts b/types/index.d.ts index 2af1b24f539..921051536c1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -178,6 +178,8 @@ declare module 'mongoose' { versionKey?: boolean; /** if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`. */ flattenMaps?: boolean; + /** if true, convert any ObjectIds in the result to 24 character hex strings. */ + flattenObjectIds?: boolean; /** If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema. */ useProjection?: boolean; } diff --git a/types/models.d.ts b/types/models.d.ts index 1efcd565063..f99492dca31 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -25,6 +25,7 @@ declare module 'mongoose' { interface MongooseBulkWriteOptions { skipValidation?: boolean; + throwOnValidationError?: boolean; } interface InsertManyOptions extends @@ -34,6 +35,7 @@ declare module 'mongoose' { rawResult?: boolean; ordered?: boolean; lean?: boolean; + throwOnValidationError?: boolean; } type InsertManyResult = mongodb.InsertManyResult & { diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 054f85e7606..7fec10b208f 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -202,5 +202,11 @@ declare module 'mongoose' { * @default { transform: true, flattenDecimals: true } */ toObject?: ToObjectOptions; + + /** + * If `true`, convert any aliases in filter, projection, update, and distinct + * to their database property names. Defaults to false. + */ + translateAliases?: boolean; } } diff --git a/types/query.d.ts b/types/query.d.ts index 540a4faf085..6815e14ad47 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -161,6 +161,11 @@ declare module 'mongoose' { * timestamps. Does nothing if schema-level timestamps are not set. */ timestamps?: boolean | QueryTimestampsConfig; + /** + * If `true`, convert any aliases in filter, projection, update, and distinct + * to their database property names. Defaults to false. + */ + translateAliases?: boolean; upsert?: boolean; useBigInt64?: boolean; writeConcern?: mongodb.WriteConcern;