From 94b308ec89fce0feaa21454337e15351b6a71c01 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 4 Oct 2022 12:55:59 +0200 Subject: [PATCH 01/10] chore(mocha-fixtures): fix spelling and remove "example" comments (#12511) * chore(mocha-fixtures): fix spelling and remove "example" comments also use "MONGOOSE_REPLSET_URI" in the false case * Update test/mocha-fixtures.js * chore(mocha-fixtures): update comment to be more descriptive Co-authored-by: Hafez --- test/mocha-fixtures.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/mocha-fixtures.js b/test/mocha-fixtures.js index 2e537071c52..fba36267bcb 100644 --- a/test/mocha-fixtures.js +++ b/test/mocha-fixtures.js @@ -12,7 +12,7 @@ let mongoinstance; // a replset-instance for running repl-set tests let mongorreplset; -// decide wheter to start a in-memory or not +// decide whether to start MMS instances or use the existing URI's const startMemoryInstance = !process.env.MONGOOSE_TEST_URI; const startMemoryReplset = !process.env.MONGOOSE_REPLSET_URI; @@ -24,11 +24,11 @@ module.exports.mochaGlobalSetup = async function mochaGlobalSetup() { // set some options when running in a CI if (process.env.CI) { + // this option is also set in the github-ci tests.yml, but this is just to ensure it is in any CI process.env.MONGOMS_PREFER_GLOBAL_PATH = '1'; // set MMS to use "~/.cache/mongodb-binaries" even when the path does not yet exist } - if (startMemoryInstance) { // Config to decided if an mongodb-memory-server instance should be used - // it's needed in global space, because we don't want to create a new instance every test-suite + if (startMemoryInstance) { mongoinstance = await mms.MongoMemoryServer.create({ instance: { args: ['--setParameter', 'ttlMonitorSleepSecs=1'], storageEngine: 'wiredTiger' } }); instanceuri = mongoinstance.getUri(); } else { @@ -39,7 +39,7 @@ module.exports.mochaGlobalSetup = async function mochaGlobalSetup() { mongorreplset = await mms.MongoMemoryReplSet.create({ replSet: { count: 3, args: ['--setParameter', 'ttlMonitorSleepSecs=1'], storageEngine: 'wiredTiger' } }); // using 3 because even numbers can lead to vote problems replseturi = mongorreplset.getUri(); } else { - replseturi = ''; + replseturi = process.env.MONGOOSE_REPLSET_URI; } process.env.MONGOOSE_TEST_URI = instanceuri; @@ -47,7 +47,7 @@ module.exports.mochaGlobalSetup = async function mochaGlobalSetup() { }; module.exports.mochaGlobalTeardown = async function mochaGlobalTeardown() { - if (mongoinstance) { // Config to decided if an mongodb-memory-server instance should be used + if (mongoinstance) { await mongoinstance.stop(); } if (mongorreplset) { From ed3a7dfba6b9dba7e7f4655be96e076a4e0bd00b Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 4 Oct 2022 17:25:39 +0200 Subject: [PATCH 02/10] fix(types): add "estimatedDocumentCount" and "countDocuments" as possible hooks plus tests fixes #12516 --- test/types/middleware.test.ts | 18 ++++++++++++++++++ types/middlewares.d.ts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts index 46c44d390e0..4b7b07d3437 100644 --- a/test/types/middleware.test.ts +++ b/test/types/middleware.test.ts @@ -92,6 +92,24 @@ schema.pre>('insertMany', function(next, docs: Array) { next(); }); +schema.pre>('count', function(next) {}); +schema.post>('count', function(count, next) { + expectType(count); + next(); +}); + +schema.pre>('estimatedDocumentCount', function(next) {}); +schema.post>('estimatedDocumentCount', function(count, next) { + expectType(count); + next(); +}); + +schema.pre>('countDocuments', function(next) {}); +schema.post>('countDocuments', function(count, next) { + expectType(count); + next(); +}); + schema.post>('findOneAndDelete', function(res, next) { expectType(res); next(); diff --git a/types/middlewares.d.ts b/types/middlewares.d.ts index 0310ca0fa0a..b8e604f2dad 100644 --- a/types/middlewares.d.ts +++ b/types/middlewares.d.ts @@ -1,7 +1,7 @@ declare module 'mongoose' { type MongooseDocumentMiddleware = 'validate' | 'save' | 'remove' | 'updateOne' | 'deleteOne' | 'init'; - type MongooseQueryMiddleware = 'count' | 'deleteMany' | 'deleteOne' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndRemove' | 'findOneAndReplace' | 'findOneAndUpdate' | 'remove' | 'replaceOne' | 'update' | 'updateOne' | 'updateMany'; + type MongooseQueryMiddleware = 'count' | 'estimatedDocumentCount' | 'countDocuments' | 'deleteMany' | 'deleteOne' | 'distinct' | 'find' | 'findOne' | 'findOneAndDelete' | 'findOneAndRemove' | 'findOneAndReplace' | 'findOneAndUpdate' | 'remove' | 'replaceOne' | 'update' | 'updateOne' | 'updateMany'; type MiddlewareOptions = { document?: boolean, query?: boolean }; type SchemaPreOptions = MiddlewareOptions; From b4aaf62577bcfbcc0e08de0ea4e79e6e1c39d727 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Oct 2022 16:43:03 -0400 Subject: [PATCH 03/10] docs(change-streams): remove unnecessary obsolete comment about needing to use mongodb driver change streams Fix #12444 --- docs/change-streams.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/change-streams.md b/docs/change-streams.md index dc7eaeb9515..bc1424ccf12 100644 --- a/docs/change-streams.md +++ b/docs/change-streams.md @@ -65,9 +65,7 @@ exports.handler = async (event, context) => { context.callbackWaitsForEmptyEventLoop = false; await connectToDatabase(); - - // Use MongoDB Node driver's `watch()` function, because Mongoose change streams - // don't support `next()` yet. See https://github.com/Automattic/mongoose/issues/11527 + const changeStream = await Country.watch([], { resumeAfter }); // Change stream `next()` will wait forever if there are no changes. So make sure to From e800193c76129179f6d2913148f70a12c8872046 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Oct 2022 16:56:15 -0400 Subject: [PATCH 04/10] docs(subdocs): clarify that populated docs are not subdocs Fix #12398 --- docs/subdocs.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/subdocs.md b/docs/subdocs.md index 64b03bd19d3..7068e9c14da 100644 --- a/docs/subdocs.md +++ b/docs/subdocs.md @@ -11,15 +11,31 @@ const childSchema = new Schema({ name: 'string' }); const parentSchema = new Schema({ // Array of subdocuments children: [childSchema], - // Single nested subdocuments. Caveat: single nested subdocs only work - // in mongoose >= 4.2.0 + // Single nested subdocuments child: childSchema }); ``` -Aside from code reuse, one important reason to use subdocuments is to create -a path where there would otherwise not be one to allow for validation over -a group of fields (e.g. dateRange.fromDate <= dateRange.toDate). +Note that populated documents are **not** subdocuments in Mongoose. +Subdocument data is embedded in the top-level document. +Referenced documents are separate top-level documents. + +```javascript +const childSchema = new Schema({ name: 'string' }); +const Child = mongoose.model('Child', childSchema); + +const parentSchema = new Schema({ + child: { + type: mongoose.ObjectId, + ref: 'Child' + } +}); +const Parent = mongoose.model('Parent', parentSchema); + +const doc = await Parent.findOne().populate('child'); +// NOT a subdocument. `doc.child` is a separate top-level document. +doc.child; +```
  • What is a Subdocument?
  • From a15242dc3ad8f5648e4f8c868589b8f427a5ef3b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Oct 2022 17:16:30 -0400 Subject: [PATCH 05/10] fix(types): indicate that Schema.prototype.discriminator() returns `this` Fix #12457 --- test/types/schema.test.ts | 8 ++------ types/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 66ed8ee28d9..e400dfa0ccf 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -570,9 +570,7 @@ export type AutoTypedSchemaType = { // discriminator const eventSchema = new Schema<{ message: string }>({ message: String }, { discriminatorKey: 'kind' }); const batchSchema = new Schema<{ name: string }>({ name: String }, { discriminatorKey: 'kind' }); -const discriminatedSchema = batchSchema.discriminator('event', eventSchema); - -expectType & { message: string }>>(discriminatedSchema); +batchSchema.discriminator('event', eventSchema); // discriminator statics const eventSchema2 = new Schema({ message: String }, { discriminatorKey: 'kind', statics: { static1: function() { @@ -581,9 +579,7 @@ const eventSchema2 = new Schema({ message: String }, { discriminatorKey: 'kind', const batchSchema2 = new Schema({ name: String }, { discriminatorKey: 'kind', statics: { static2: function() { return 1; } } }); -const discriminatedSchema2 = batchSchema2.discriminator('event', eventSchema2); - -expectAssignable & { message: string }, Model, {}, {}, {}, { static1(): number; static2(): number; }>>(discriminatedSchema2); +batchSchema2.discriminator('event', eventSchema2); function gh11828() { interface IUser { diff --git a/types/index.d.ts b/types/index.d.ts index ba560b634dd..5fc0853987b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -208,7 +208,7 @@ declare module 'mongoose' { /** Returns a copy of this schema */ clone(): T; - discriminator(name: string, schema: DisSchema): DiscriminatorSchema; + discriminator(name: string, schema: DisSchema): this; /** Returns a new schema that has the picked `paths` from this schema. */ pick(paths: string[], options?: SchemaOptions): T; From 167f68284098dcb67d733edc540cce596db44d4d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 4 Oct 2022 18:26:03 -0400 Subject: [PATCH 06/10] fix(document): set defaults on subdocuments underneath init-ed single nested subdocument Fix #12515 --- lib/schema/SubdocumentPath.js | 1 + test/document.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index e748d6a5026..46ea86673e0 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -174,6 +174,7 @@ SubdocumentPath.prototype.cast = function(val, doc, init, priorVal, options) { subdoc = new Constructor(void 0, selected, doc, false, { defaults: false }); subdoc.$init(val); applyDefaults(subdoc, selected); + delete subdoc.$__.defaults; } else { if (Object.keys(val).length === 0) { return new Constructor({}, selected, doc, undefined, options); diff --git a/test/document.test.js b/test/document.test.js index ca2aca278d9..eebea364b3c 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11865,6 +11865,33 @@ describe('document', function() { assert.strictEqual(called, 1); }); + it('applies defaults to pushed subdocs after initing document (gh-12515)', async function() { + const animalSchema = new Schema({ title: String }); + const animalsSchema = new Schema({ + species: [animalSchema], + totalAnimals: Number + }); + const Userschema = new Schema({ + animals: animalsSchema + }); + const UserModel = db.model('User', Userschema); + + const doc = new UserModel(); + doc.animals = { totalAnimals: 1 }; + doc.animals.species = [{ title: 'Lion' }]; + await doc.save(); + // once created we fetch it again + let user = await UserModel.findById(doc._id); + + // add new animal + user.animals.species.push({ title: 'Elephant' }); + await user.save(); + assert.ok(user.animals.species[1]._id); + user = await UserModel.collection.findOne({ _id: user._id }); + + assert.ok(user.animals.species[1]._id); + }); + it('If the field does not exist, $inc should create it and set is value to the specified one (gh-12435)', async function() { const schema = new mongoose.Schema({ name: String, From b0edca9f6d2ce957194f8229744140c08b325fb5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Oct 2022 10:25:14 -0400 Subject: [PATCH 07/10] fix: quick fix so `defaults` is unset before init --- lib/schema/SubdocumentPath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js index 46ea86673e0..1eb65207d85 100644 --- a/lib/schema/SubdocumentPath.js +++ b/lib/schema/SubdocumentPath.js @@ -172,9 +172,9 @@ SubdocumentPath.prototype.cast = function(val, doc, init, priorVal, options) { options = Object.assign({}, options, { priorDoc: priorVal }); if (init) { subdoc = new Constructor(void 0, selected, doc, false, { defaults: false }); + delete subdoc.$__.defaults; subdoc.$init(val); applyDefaults(subdoc, selected); - delete subdoc.$__.defaults; } else { if (Object.keys(val).length === 0) { return new Constructor({}, selected, doc, undefined, options); From 9093f7cda8938edc219298ab3083ee92ee00f30e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Oct 2022 11:00:20 -0400 Subject: [PATCH 08/10] docs(models): add section on MongoDB Views Fix #5694 --- docs/models.md | 67 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/docs/models.md b/docs/models.md index f72f716154a..b68f2eac7e7 100644 --- a/docs/models.md +++ b/docs/models.md @@ -1,4 +1,4 @@ -## Models +# Models [Models](./api.html#model-js) are fancy constructors compiled from `Schema` definitions. An instance of a model is called a @@ -11,6 +11,7 @@ reading documents from the underlying MongoDB database. * [Deleting](#deleting) * [Updating](#updating) * [Change Streams](#change-streams) +* [Views](#views)

    Compiling your first model

    @@ -31,7 +32,7 @@ in the database. you've added everything you want to `schema`, including hooks, before calling `.model()`! -### Constructing Documents +## Constructing Documents An instance of a model is called a [document](./documents.html). Creating them and saving to the database is easy. @@ -73,7 +74,7 @@ const connection = mongoose.createConnection('mongodb://localhost:27017/test'); const Tank = connection.model('Tank', yourSchema); ``` -### Querying +## Querying Finding documents is easy with Mongoose, which supports the [rich](http://www.mongodb.org/display/DOCS/Advanced+Queries) query syntax of MongoDB. Documents can be retrieved using a `model`'s [find](./api.html#model_Model-find), [findById](./api.html#model_Model-findById), [findOne](./api.html#model_Model-findOne), or [where](./api.html#model_Model-where) static methods. @@ -83,7 +84,7 @@ Tank.find({ size: 'small' }).where('createdDate').gt(oneYearAgo).exec(callback); See the chapter on [queries](./queries.html) for more details on how to use the [Query](./api.html#query-js) api. -### Deleting +## Deleting Models have static `deleteOne()` and `deleteMany()` functions for removing all documents matching the given `filter`. @@ -95,7 +96,7 @@ Tank.deleteOne({ size: 'large' }, function (err) { }); ``` -### Updating +## Updating Each `model` has its own `update` method for modifying documents in the database without returning them to your application. See the @@ -112,9 +113,7 @@ _If you want to update a single document in the db and return it to your application, use [findOneAndUpdate](./api.html#model_Model-findOneAndUpdate) instead._ -### Change Streams - -_New in MongoDB 3.6.0 and Mongoose 5.0.0_ +## Change Streams [Change streams](https://docs.mongodb.com/manual/changeStreams/) provide a way for you to listen to all inserts and updates going through your @@ -154,10 +153,58 @@ The output from the above [async function](http://thecodebarbarian.com/80-20-gui You can read more about [change streams in mongoose in this blog post](http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-change-streams.html#change-streams-in-mongoose). -### Yet more +## Views + +[MongoDB Views](https://www.mongodb.com/docs/manual/core/views) are essentially read-only collections that contain data computed from other collections using [aggregations](./api/aggregate.html). +In Mongoose, you should define a separate Model for each of your Views. +You can also create a View using [`createCollection()`](./api/model.html#model_Model-createCollection). + +The following example shows how you can create a new `RedactedUser` View on a `User` Model that hides potentially sensitive information, like name and email. + +```javascript +// Make sure to disable `autoCreate` and `autoIndex` for Views, +// because you want to create the collection manually. +const userSchema = new Schema({ + name: String, + email: String, + roles: [String] +}, { autoCreate: false, autoIndex: false }); +const User = mongoose.model('User', userSchema); + +const RedactedUser = mongoose.model('RedactedUser', userSchema); + +// First, create the User model's underlying collection... +await User.createCollection(); +// Then create the `RedactedUser` model's underlying collection +// as a View. +await RedactedUser.createCollection({ + viewOn: 'users', // Set `viewOn` to the collection name, **not** model name. + pipeline: [ + { + $set: { + name: { $concat: [{ $substr: ['$name', 0, 3] }, '...'] }, + email: { $concat: [{ $substr: ['$email', 0, 3] }, '...'] } + } + } + ] +}); + +await User.create([ + { name: 'John Smith', email: 'john.smith@gmail.com', roles: ['user'] }, + { name: 'Bill James', email: 'bill@acme.co', roles: ['user', 'admin'] } +]); + +// [{ _id: ..., name: 'Bil...', email: 'bil...', roles: ['user', 'admin'] }] +console.log(await RedactedUser.find({ roles: 'admin' })); +``` + +Note that Mongoose does **not** currently enforce that Views are read-only. +If you attempt to `save()` a document from a View, you will get an error from the MongoDB server. + +## Yet more The [API docs](./api.html#model_Model) cover many additional methods available like [count](./api.html#model_Model-count), [mapReduce](./api.html#model_Model-mapReduce), [aggregate](./api.html#model_Model-aggregate), and [more](./api.html#model_Model-findOneAndRemove). -### Next Up +## Next Up Now that we've covered `Models`, let's take a look at [Documents](/docs/documents.html). From 630bd3738ea237934ac4f246fb97d868412b5d70 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Oct 2022 11:04:04 -0400 Subject: [PATCH 09/10] test: cover a couple of extra cases for #12515 --- test/document.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/document.test.js b/test/document.test.js index eebea364b3c..2d38adc8629 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11886,9 +11886,11 @@ describe('document', function() { // add new animal user.animals.species.push({ title: 'Elephant' }); await user.save(); + assert.ok(user.animals.species[0]._id); assert.ok(user.animals.species[1]._id); user = await UserModel.collection.findOne({ _id: user._id }); + assert.ok(user.animals.species[0]._id); assert.ok(user.animals.species[1]._id); }); From 2691f74df958bdf9b5dfeb6ce6b4e340df7c8a7d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Oct 2022 11:30:00 -0400 Subject: [PATCH 10/10] Update docs/models.md Co-authored-by: hasezoey --- docs/models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/models.md b/docs/models.md index b68f2eac7e7..565cc2d8a7b 100644 --- a/docs/models.md +++ b/docs/models.md @@ -13,7 +13,7 @@ reading documents from the underlying MongoDB database. * [Change Streams](#change-streams) * [Views](#views) -

    Compiling your first model

    +

    Compiling your first model

    When you call `mongoose.model()` on a schema, Mongoose _compiles_ a model for you.