diff --git a/index.js b/index.js index f44ddb21f3..6ebbd5fd5d 100644 --- a/index.js +++ b/index.js @@ -46,7 +46,6 @@ module.exports.Decimal128 = mongoose.Decimal128; module.exports.Mixed = mongoose.Mixed; module.exports.Date = mongoose.Date; module.exports.Number = mongoose.Number; -module.exports.Double = mongoose.Double; module.exports.Error = mongoose.Error; module.exports.MongooseError = mongoose.MongooseError; module.exports.now = mongoose.now; diff --git a/lib/connection.js b/lib/connection.js index c2cb1b49b3..85f148fcaa 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -158,7 +158,7 @@ Object.defineProperty(Connection.prototype, 'readyState', { * @api public */ -Connection.prototype.get = function(key) { +Connection.prototype.get = function getOption(key) { if (this.config.hasOwnProperty(key)) { return this.config[key]; } @@ -186,7 +186,7 @@ Connection.prototype.get = function(key) { * @api public */ -Connection.prototype.set = function(key, val) { +Connection.prototype.set = function setOption(key, val) { if (this.config.hasOwnProperty(key)) { this.config[key] = val; return val; @@ -925,7 +925,7 @@ Connection.prototype._shouldBufferCommands = function _shouldBufferCommands() { * @api private */ -Connection.prototype.error = function(err, callback) { +Connection.prototype.error = function error(err, callback) { if (callback) { callback(err); return null; @@ -939,6 +939,7 @@ Connection.prototype.error = function(err, callback) { /** * Called when the connection is opened * + * @emits "open" * @api private */ @@ -1043,11 +1044,21 @@ Connection.prototype.openUri = async function openUri(uri, options) { return this; }; -/*! - * Treat `on('error')` handlers as handling the initialConnection promise - * to avoid uncaught exceptions when using `on('error')`. See gh-14377. +/** + * Listen to events in the Connection + * + * @param {String} event The event to listen on + * @param {Function} callback + * @see Connection#readyState https://mongoosejs.com/docs/api/connection.html#Connection.prototype.readyState + * + * @method on + * @instance + * @memberOf Connection + * @api public */ +// Treat `on('error')` handlers as handling the initialConnection promise +// to avoid uncaught exceptions when using `on('error')`. See gh-14377. Connection.prototype.on = function on(event, callback) { if (event === 'error' && this.$initialConnection) { this.$initialConnection.catch(() => {}); @@ -1055,11 +1066,21 @@ Connection.prototype.on = function on(event, callback) { return EventEmitter.prototype.on.call(this, event, callback); }; -/*! - * Treat `once('error')` handlers as handling the initialConnection promise - * to avoid uncaught exceptions when using `on('error')`. See gh-14377. +/** + * Listen to a event once in the Connection + * + * @param {String} event The event to listen on + * @param {Function} callback + * @see Connection#readyState https://mongoosejs.com/docs/api/connection.html#Connection.prototype.readyState + * + * @method once + * @instance + * @memberOf Connection + * @api public */ +// Treat `on('error')` handlers as handling the initialConnection promise +// to avoid uncaught exceptions when using `on('error')`. See gh-14377. Connection.prototype.once = function on(event, callback) { if (event === 'error' && this.$initialConnection) { this.$initialConnection.catch(() => {}); @@ -1220,17 +1241,18 @@ Connection.prototype._close = async function _close(force, destroy) { * @api private */ -Connection.prototype.doClose = function() { +Connection.prototype.doClose = function doClose() { throw new Error('Connection#doClose unimplemented by driver'); }; /** * Called when the connection closes * + * @emits "close" * @api private */ -Connection.prototype.onClose = function(force) { +Connection.prototype.onClose = function onClose(force) { this.readyState = STATES.disconnected; // avoid having the collection subscribe to our event emitter @@ -1334,7 +1356,7 @@ Connection.prototype.plugin = function(fn, opts) { * @api public */ -Connection.prototype.model = function(name, schema, collection, options) { +Connection.prototype.model = function model(name, schema, collection, options) { if (!(this instanceof Connection)) { throw new MongooseError('`connection.model()` should not be run with ' + '`new`. If you are doing `new db.model(foo)(bar)`, use ' + @@ -1454,7 +1476,7 @@ Connection.prototype.model = function(name, schema, collection, options) { * @return {Connection} this */ -Connection.prototype.deleteModel = function(name) { +Connection.prototype.deleteModel = function deleteModel(name) { if (typeof name === 'string') { const model = this.model(name); if (model == null) { @@ -1510,7 +1532,7 @@ Connection.prototype.deleteModel = function(name) { * @return {ChangeStream} mongoose-specific change stream wrapper, inherits from EventEmitter */ -Connection.prototype.watch = function(pipeline, options) { +Connection.prototype.watch = function watch(pipeline, options) { const changeStreamThunk = cb => { immediate(() => { if (this.readyState === STATES.connecting) { @@ -1559,7 +1581,7 @@ Connection.prototype.asPromise = async function asPromise() { * @return {String[]} */ -Connection.prototype.modelNames = function() { +Connection.prototype.modelNames = function modelNames() { return Object.keys(this.models); }; @@ -1571,7 +1593,7 @@ Connection.prototype.modelNames = function() { * @api private * @return {Boolean} true if the connection should be authenticated after it is opened, otherwise false. */ -Connection.prototype.shouldAuthenticate = function() { +Connection.prototype.shouldAuthenticate = function shouldAuthenticate() { return this.user != null && (this.pass != null || this.authMechanismDoesNotRequirePassword()); }; @@ -1584,7 +1606,7 @@ Connection.prototype.shouldAuthenticate = function() { * @return {Boolean} true if the authentication mechanism specified in the options object requires * a password, otherwise false. */ -Connection.prototype.authMechanismDoesNotRequirePassword = function() { +Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanismDoesNotRequirePassword() { if (this.options && this.options.auth) { return noPasswordAuthMechanisms.indexOf(this.options.auth.authMechanism) >= 0; } @@ -1602,7 +1624,7 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function() { * @return {Boolean} true if the provided options object provides enough data to authenticate with, * otherwise false. */ -Connection.prototype.optionsProvideAuthenticationData = function(options) { +Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) { return (options) && (options.user) && ((options.pass) || this.authMechanismDoesNotRequirePassword()); diff --git a/lib/cursor/aggregationCursor.js b/lib/cursor/aggregationCursor.js index fd795526ca..2cff8bb8e1 100644 --- a/lib/cursor/aggregationCursor.js +++ b/lib/cursor/aggregationCursor.js @@ -175,11 +175,10 @@ AggregationCursor.prototype._markError = function(error) { * Marks this cursor as closed. Will stop streaming and subsequent calls to * `next()` will error. * - * @param {Function} callback * @return {Promise} * @api public * @method close - * @emits close + * @emits "close" * @see AggregationCursor.close https://mongodb.github.io/node-mongodb-native/4.9/classes/AggregationCursor.html#close */ diff --git a/lib/document.js b/lib/document.js index ea0e6f7a37..dcf669fdfd 100644 --- a/lib/document.js +++ b/lib/document.js @@ -831,7 +831,7 @@ function init(self, obj, doc, opts, prefix) { * * #### Example: * - * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }, callback); + * weirdCar.updateOne({$inc: {wheels:1}}, { w: 1 }); * * #### Valid options: * @@ -843,7 +843,6 @@ function init(self, obj, doc, opts, prefix) { * @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and the [Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html). * @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 {Function} [callback] * @return {Query} * @api public * @memberOf Document @@ -3444,12 +3443,11 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) { * @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern). * @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names) * @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`. - * @param {Function} [fn] optional callback * @method save * @memberOf Document * @instance * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew()) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating). - * @return {Promise|undefined} Returns undefined if used with callback or a Promise otherwise. + * @return {Promise} * @api public * @see middleware https://mongoosejs.com/docs/middleware.html */ diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 408da3d0e7..d2150c6ad5 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -454,7 +454,6 @@ function format(obj, sub, color, shell) { /** * Retrieves information about this collections indexes. * - * @param {Function} callback * @method getIndexes * @api public */ diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js index 7c8633ab5d..f5c421656d 100644 --- a/lib/helpers/model/discriminator.js +++ b/lib/helpers/model/discriminator.js @@ -119,6 +119,7 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu // schema. `Schema.prototype.clone()` copies `obj` by reference, no cloning. schema.obj = { ...schema.obj }; mergeDiscriminatorSchema(schema, baseSchema); + schema._gatherChildSchemas(); // Clean up conflicting paths _after_ merging re: gh-6076 for (const conflictingPath of conflictingPaths) { diff --git a/lib/model.js b/lib/model.js index 9f347ac41c..3ad42386cb 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2231,7 +2231,7 @@ Model.$where = function $where() { /** * 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. + * Finds a matching document, updates it according to the `update` arg, passing any `options`. A Query object is returned. * * #### Example: * @@ -3643,7 +3643,11 @@ Model.castObject = function castObject(obj, options) { options = options || {}; const ret = {}; - const schema = this.schema; + let schema = this.schema; + const discriminatorKey = schema.options.discriminatorKey; + if (schema.discriminators != null && obj != null && obj[discriminatorKey] != null) { + schema = getSchemaDiscriminatorByValue(schema, obj[discriminatorKey]) || schema; + } const paths = Object.keys(schema.paths); for (const path of paths) { @@ -3992,7 +3996,7 @@ function _update(model, op, conditions, doc, options) { /** * Performs [aggregations](https://www.mongodb.com/docs/manual/aggregation/) on the models collection. * - * If a `callback` is passed, the `aggregate` is executed and a `Promise` is returned. If a callback is not passed, the `aggregate` itself is returned. + * The `aggregate` itself is returned. * * This function triggers the following middleware. * @@ -4047,10 +4051,6 @@ Model.aggregate = function aggregate(pipeline, options) { aggregate.option(options); } - if (typeof callback === 'undefined') { - return aggregate; - } - return aggregate; }; @@ -4238,7 +4238,6 @@ Model.validate = async function validate(obj, pathsOrOptions, context) { * @param {Object} [options.options=null] Additional options like `limit` and `lean`. * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document. * @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated - * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`. * @return {Promise} * @api public */ diff --git a/lib/mongoose.js b/lib/mongoose.js index 82bd9f50ae..f3b5f0948b 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -245,7 +245,7 @@ Mongoose.prototype.setDriver = function setDriver(driver) { * @api public */ -Mongoose.prototype.set = function(key, value) { +Mongoose.prototype.set = function getsetOptions(key, value) { const _mongoose = this instanceof Mongoose ? this : mongoose; if (arguments.length === 1 && typeof key !== 'object') { @@ -376,7 +376,7 @@ Mongoose.prototype.get = Mongoose.prototype.set; * @api public */ -Mongoose.prototype.createConnection = function(uri, options) { +Mongoose.prototype.createConnection = function createConnection(uri, options) { const _mongoose = this instanceof Mongoose ? this : mongoose; const Connection = _mongoose.__driver.Connection; @@ -427,7 +427,6 @@ Mongoose.prototype.createConnection = function(uri, options) { * @param {Number} [options.socketTimeoutMS=0] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. A socket may be inactive because of either no activity or a long-running operation. `socketTimeoutMS` defaults to 0, which means Node.js will not time out the socket due to inactivity. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes. * @param {Number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both. * @param {Boolean} [options.autoCreate=false] Set to `true` to make Mongoose automatically call `createCollection()` on every model created on this connection. - * @param {Function} [callback] * @see Mongoose#createConnection https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.createConnection() * @api public * @return {Promise} resolves to `this` if connection succeeded @@ -479,12 +478,11 @@ Mongoose.prototype.disconnect = async function disconnect() { * * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/MongoClient.html#startSession) * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency - * @param {Function} [callback] * @return {Promise} promise that resolves to a MongoDB driver `ClientSession` * @api public */ -Mongoose.prototype.startSession = function() { +Mongoose.prototype.startSession = function startSession() { const _mongoose = this instanceof Mongoose ? this : mongoose; return _mongoose.connection.startSession.apply(_mongoose.connection, arguments); @@ -498,7 +496,7 @@ Mongoose.prototype.startSession = function() { * @api public */ -Mongoose.prototype.pluralize = function(fn) { +Mongoose.prototype.pluralize = function pluralize(fn) { const _mongoose = this instanceof Mongoose ? this : mongoose; if (arguments.length > 0) { @@ -561,7 +559,7 @@ Mongoose.prototype.pluralize = function(fn) { * @api public */ -Mongoose.prototype.model = function(name, schema, collection, options) { +Mongoose.prototype.model = function model(name, schema, collection, options) { const _mongoose = this instanceof Mongoose ? this : mongoose; if (typeof schema === 'string') { @@ -572,7 +570,7 @@ Mongoose.prototype.model = function(name, schema, collection, options) { if (arguments.length === 1) { const model = _mongoose.models[name]; if (!model) { - throw new MongooseError.MissingSchemaError(name); + throw new _mongoose.Error.MissingSchemaError(name); } return model; } @@ -581,7 +579,7 @@ Mongoose.prototype.model = function(name, schema, collection, options) { schema = new Schema(schema); } if (schema && !(schema instanceof Schema)) { - throw new Error('The 2nd parameter to `mongoose.model()` should be a ' + + throw new _mongoose.Error('The 2nd parameter to `mongoose.model()` should be a ' + 'schema or a POJO'); } @@ -632,7 +630,7 @@ Mongoose.prototype.model = function(name, schema, collection, options) { * ignore */ -Mongoose.prototype._model = function(name, schema, collection, options) { +Mongoose.prototype._model = function _model(name, schema, collection, options) { const _mongoose = this instanceof Mongoose ? this : mongoose; let model; @@ -707,7 +705,7 @@ Mongoose.prototype._model = function(name, schema, collection, options) { * @return {Mongoose} this */ -Mongoose.prototype.deleteModel = function(name) { +Mongoose.prototype.deleteModel = function deleteModel(name) { const _mongoose = this instanceof Mongoose ? this : mongoose; _mongoose.connection.deleteModel(name); @@ -726,7 +724,7 @@ Mongoose.prototype.deleteModel = function(name) { * @return {Array} */ -Mongoose.prototype.modelNames = function() { +Mongoose.prototype.modelNames = function modelNames() { const _mongoose = this instanceof Mongoose ? this : mongoose; const names = Object.keys(_mongoose.models); @@ -740,7 +738,7 @@ Mongoose.prototype.modelNames = function() { * @api private */ -Mongoose.prototype._applyPlugins = function(schema, options) { +Mongoose.prototype._applyPlugins = function _applyPlugins(schema, options) { const _mongoose = this instanceof Mongoose ? this : mongoose; options = options || {}; @@ -763,7 +761,7 @@ Mongoose.prototype._applyPlugins = function(schema, options) { * @api public */ -Mongoose.prototype.plugin = function(fn, opts) { +Mongoose.prototype.plugin = function plugin(fn, opts) { const _mongoose = this instanceof Mongoose ? this : mongoose; _mongoose.plugins.push([fn, opts]); @@ -1073,7 +1071,7 @@ Mongoose.prototype.ObjectId = SchemaTypes.ObjectId; * @api public */ -Mongoose.prototype.isValidObjectId = function(v) { +Mongoose.prototype.isValidObjectId = function isValidObjectId(v) { const _mongoose = this instanceof Mongoose ? this : mongoose; return _mongoose.Types.ObjectId.isValid(v); }; @@ -1105,7 +1103,7 @@ Mongoose.prototype.isValidObjectId = function(v) { * @api public */ -Mongoose.prototype.isObjectIdOrHexString = function(v) { +Mongoose.prototype.isObjectIdOrHexString = function isObjectIdOrHexString(v) { return isBsonType(v, 'ObjectId') || (typeof v === 'string' && objectIdHexRegexp.test(v)); }; @@ -1117,7 +1115,7 @@ Mongoose.prototype.isObjectIdOrHexString = function(v) { * @param {Boolean} options.continueOnError `false` by default. If set to `true`, mongoose will not throw an error if one model syncing failed, and will return an object where the keys are the names of the models, and the values are the results/errors for each model. * @return {Promise} Returns a Promise, when the Promise resolves the value is a list of the dropped indexes. */ -Mongoose.prototype.syncIndexes = function(options) { +Mongoose.prototype.syncIndexes = function syncIndexes(options) { const _mongoose = this instanceof Mongoose ? this : mongoose; return _mongoose.connection.syncIndexes(options); }; @@ -1192,8 +1190,8 @@ Mongoose.prototype.Number = SchemaTypes.Number; * @api public */ -Mongoose.prototype.Error = require('./error/index'); -Mongoose.prototype.MongooseError = require('./error/mongooseError'); +Mongoose.prototype.Error = MongooseError; +Mongoose.prototype.MongooseError = MongooseError; /** * Mongoose uses this function to get the current time when setting @@ -1218,7 +1216,7 @@ Mongoose.prototype.now = function now() { return new Date(); }; * @api public */ -Mongoose.prototype.CastError = require('./error/cast'); +Mongoose.prototype.CastError = MongooseError.CastError; /** * The constructor used for schematype options diff --git a/lib/query.js b/lib/query.js index d50f913cc7..4a4dc5aca0 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3197,7 +3197,6 @@ Query.prototype.deleteMany = function(filter, options) { /** * Execute a `deleteMany()` query * - * @param {Function} callback * @method _deleteMany * @instance * @memberOf Query @@ -3490,13 +3489,6 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() { * - `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 * - * #### Callback Signature - * - * function(error, doc) { - * // error: any errors that occurred - * // doc: the document before updates are applied if `new: false`, or after updates if `new = true` - * } - * * #### Example: * * A.where().findOneAndDelete(conditions, options) // return Query @@ -3587,13 +3579,6 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() { * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0 * - `includeResultMetadata`: if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document * - * #### Callback Signature - * - * function(error, doc) { - * // error: any errors that occurred - * // doc: the document before updates are applied if `new: false`, or after updates if `new = true` - * } - * * #### Example: * * A.where().findOneAndReplace(filter, replacement, options); // return Query @@ -4025,7 +4010,6 @@ Query.prototype._replaceOne = async function _replaceOne() { * @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 {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators. - * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() * @see Query docs https://mongoosejs.com/docs/queries.html @@ -4096,7 +4080,6 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * @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 {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert). Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators. - * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() * @see Query docs https://mongoosejs.com/docs/queries.html @@ -4163,7 +4146,6 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) { * @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() * @see Query docs https://mongoosejs.com/docs/queries.html diff --git a/lib/schema.js b/lib/schema.js index 0d2f579d8d..2915bf5f5a 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -441,7 +441,7 @@ Schema.prototype._clone = function _clone(Constructor) { } } } - s.childSchemas = gatherChildSchemas(s); + s._gatherChildSchemas(); s.virtuals = clone(this.virtuals); s.$globalPluginsApplied = this.$globalPluginsApplied; @@ -1238,14 +1238,14 @@ Schema.prototype.path = function(path, obj) { * ignore */ -function gatherChildSchemas(schema) { +Schema.prototype._gatherChildSchemas = function _gatherChildSchemas() { const childSchemas = []; - for (const path of Object.keys(schema.paths)) { + for (const path of Object.keys(this.paths)) { if (typeof path !== 'string') { continue; } - const schematype = schema.paths[path]; + const schematype = this.paths[path]; if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) { childSchemas.push({ schema: schematype.schema, @@ -1261,8 +1261,9 @@ function gatherChildSchemas(schema) { } } + this.childSchemas = childSchemas; return childSchemas; -} +}; /*! * ignore @@ -2145,6 +2146,12 @@ Schema.prototype.index = function(fields, options) { } } + for (const existingIndex of this.indexes()) { + if (util.isDeepStrictEqual(existingIndex[0], fields)) { + throw new MongooseError(`Schema already has an index on ${JSON.stringify(fields)}`); + } + } + this._indexes.push([fields, options]); return this; }; diff --git a/lib/schema/string.js b/lib/schema/string.js index e7eed1c4f0..d62e233765 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -181,11 +181,11 @@ SchemaString.checkRequired = SchemaType.checkRequired; * const s = new Schema({ state: { type: String, enum: states }}) * const M = db.model('M', s) * const m = new M({ state: 'invalid' }) - * m.save(function (err) { - * console.error(String(err)) // ValidationError: `invalid` is not a valid enum value for path `state`. - * m.state = 'open' - * m.save(callback) // success - * }) + * await m.save() + * .catch((err) => console.error(err)); // ValidationError: `invalid` is not a valid enum value for path `state`. + * m.state = 'open'; + * await m.save(); + * // success * * // or with custom error messages * const enum = { @@ -195,11 +195,11 @@ SchemaString.checkRequired = SchemaType.checkRequired; * const s = new Schema({ state: { type: String, enum: enum }) * const M = db.model('M', s) * const m = new M({ state: 'invalid' }) - * m.save(function (err) { - * console.error(String(err)) // ValidationError: enum validator failed for path `state` with value `invalid` - * m.state = 'open' - * m.save(callback) // success - * }) + * await m.save() + * .catch((err) => console.error(err)); // ValidationError: enum validator failed for path `state` with value `invalid` + * m.state = 'open'; + * await m.save(); + * // success * * @param {...String|Object} [args] enumeration values * @return {SchemaType} this diff --git a/package.json b/package.json index f2f16db7f9..2d38af930e 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "devDependencies": { "@babel/core": "7.26.0", "@babel/preset-env": "7.26.0", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", "acquit": "1.3.0", "acquit-ignore": "0.2.1", "acquit-require": "0.1.1", diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index a829211b23..25e289726e 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -2251,4 +2251,129 @@ describe('model', function() { assert(result); }); + + it('correctly gathers subdocs with discriminators (gh-15088)', async function() { + const RequestTriggeredWorkflowSchema = new Schema( + { + workflow: { + type: String + } + }, + { + timestamps: true + } + ); + + const RequestSchema = new Schema( + { + status: { + type: String + }, + summary: { + type: String + }, + triggeredWorkflows: [RequestTriggeredWorkflowSchema] + }, + { + discriminatorKey: 'source', + timestamps: true + } + ); + + const EmailRequestSchema = new Schema({ + // Any extra properties that specifically apply to 'email' requests + }); + const FormRequestSchema = new Schema({ + // Any extra properties that specifically apply to 'form' requests + }); + + const Request = db.model('Request', RequestSchema); + + Request.discriminator('Request:email', EmailRequestSchema, 'email'); + Request.discriminator('Request:form', FormRequestSchema, 'form'); + + const request = await Request.create({ + status: 'new', + source: 'form' + }); + + let requestAsReadFromDb = await Request.findById(request._id); + + request.status = 'in progress'; + await request.save(); + + assert.equal(requestAsReadFromDb.$getAllSubdocs().length, 0); + const now = new Date(); + request.triggeredWorkflows.push({ + workflow: '111111111111111111111111' + }); + assert.equal(request.$getAllSubdocs().length, 1); + assert.equal(request.$getAllSubdocs()[0], request.triggeredWorkflows[0]); + await request.save(); + + requestAsReadFromDb = await Request.findById(request._id); + assert.equal(requestAsReadFromDb.$getAllSubdocs().length, 1); + assert.equal(requestAsReadFromDb.$getAllSubdocs()[0], requestAsReadFromDb.triggeredWorkflows[0]); + assert.ok(requestAsReadFromDb.triggeredWorkflows[0].createdAt.valueOf() >= now.valueOf()); + assert.ok(requestAsReadFromDb.triggeredWorkflows[0].updatedAt.valueOf() >= now.valueOf()); + }); + + it('triggers save hooks on subdocuments (gh-15092)', async function() { + const subdocumentSchema = mongoose.Schema({ + name: String + }); + + const subdocumentPreSaveHooks = []; + subdocumentSchema.pre('save', function(next) { + subdocumentPreSaveHooks.push(this); + next(); + }); + + const schema = mongoose.Schema({ + name: String, + subdocuments: [subdocumentSchema] + }, { discriminatorKey: 'type' }); + + const documentPreSaveHooks = []; + schema.pre('save', function(next) { + documentPreSaveHooks.push(this); + next(); + }); + + const Document = db.model('Document', schema); + + const discriminatorSchema = mongoose.Schema({}); + + const discriminatorPreSaveHooks = []; + discriminatorSchema.pre('save', function(next) { + discriminatorPreSaveHooks.push(this); + next(); + }); + + const Discriminator = Document.discriminator('Discriminator', discriminatorSchema); + + const document = new Document({ name: 'Document' }); + document.subdocuments.push({ name: 'Document Subdocument' }); + await document.save(); + + assert.equal(discriminatorPreSaveHooks.length, 0); + assert.equal(subdocumentPreSaveHooks.length, 1); + assert.equal(subdocumentPreSaveHooks[0], document.subdocuments[0]); + assert.equal(documentPreSaveHooks.length, 1); + assert.equal(documentPreSaveHooks[0], document); + + subdocumentPreSaveHooks.length = 0; + documentPreSaveHooks.length = 0; + + const discriminator = new Discriminator({ name: 'Discriminator', type: 'Discriminator' }); + discriminator.subdocuments.push({ name: 'Discriminator Subdocument' }); + await discriminator.save(); + + assert.equal(subdocumentPreSaveHooks.length, 1); + assert.equal(subdocumentPreSaveHooks[0], discriminator.subdocuments[0]); + assert.equal(discriminatorPreSaveHooks.length, 1); + assert.equal(discriminatorPreSaveHooks[0], discriminator); + assert.equal(documentPreSaveHooks.length, 1); + assert.equal(documentPreSaveHooks[0], discriminator); + }); }); diff --git a/test/model.test.js b/test/model.test.js index 73d2e809ef..673834cda9 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7717,6 +7717,67 @@ describe('Model', function() { const ret = Test.castObject(obj, { ignoreCastErrors: true }); assert.deepStrictEqual(ret, { nested: { num: 2 }, docArr: [{ num: 4 }] }); }); + it('handles discriminators (gh-15075)', async function() { + // Create the base shape schema + const shapeSchema = new mongoose.Schema({ name: String }, { + discriminatorKey: 'kind', + _id: false + }); + + // Main schema with shape array + const schema = new mongoose.Schema({ + shape: [shapeSchema] + }); + + // Circle discriminator + schema + .path('shape') + .discriminator('Circle', new mongoose.Schema({ + radius: { + type: mongoose.Schema.Types.Number, + required: true + } + }, { _id: false })); + + // PropertyPath schema for Square + const propertyPathSchema = new mongoose.Schema({ + property: { + type: mongoose.Schema.Types.String, + required: true + }, + path: { + type: mongoose.Schema.Types.String, + required: true + } + }, { _id: false }); + + // Square discriminator + schema + .path('shape') + .discriminator( + 'Square', + new mongoose.Schema({ + propertyPaths: { + type: [propertyPathSchema], + required: true + } + }, { _id: false }) + ); + + const TestModel = db.model('Test', schema); + + const circle = { shape: [{ kind: 'Circle', radius: '5' }] }; + const square = { shape: [{ kind: 'Square', propertyPaths: [{ property: 42 }] }] }; + + assert.deepStrictEqual( + TestModel.castObject(circle).shape[0], + { kind: 'Circle', radius: 5 } + ); + assert.deepStrictEqual( + TestModel.castObject(square).shape[0], + { kind: 'Square', propertyPaths: [{ property: '42' }] } + ); + }); }); it('works if passing class that extends Document to `loadClass()` (gh-12254)', async function() { diff --git a/test/schema.test.js b/test/schema.test.js index 3076c9df62..6378ec340a 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -3278,4 +3278,21 @@ describe('schema', function() { assert.ok(subdoc instanceof mongoose.Document); assert.equal(subdoc.getAnswer(), 42); }); + it('throws "already has an index" error if duplicate index definition (gh-15056)', function() { + const ObjectKeySchema = new mongoose.Schema({ + key: { + type: String, + required: true, + unique: true + }, + type: { + type: String, + required: false + } + }); + + assert.throws(() => { + ObjectKeySchema.index({ key: 1 }); + }, /MongooseError.*already has an index/); + }); });