Skip to content

Commit dfaf1a2

Browse files
committed
Merge branch 'ownerDoc' of github.com:Automattic/mongoose into ownerDoc
2 parents 8d1da80 + ed1ba9e commit dfaf1a2

14 files changed

+179
-26
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
6.0.14 / 2021-11-29
2+
===================
3+
* fix(document): catch errors in required functions #10968
4+
* fix(connection): clone schema when passing a schema from a different copy of Mongoose to `Connection#model()` #10904
5+
* fix(populate): set empty array [] on virtual populate with no result #10992
6+
* fix(query): handle orFail() with replaceOne() #10963
7+
* fix(populate): use Model by default when using Model.populate() on a POJO #10978
8+
* fix(document): throw VersionError if saving a document with version bump and document isn't found #10974
9+
* fix(index.d.ts): make populate type param optional #10989 [mohd-akram](https://github.com/mohd-akram)
10+
* docs(migrating_to_6): add a note about minimize and toObject() behavior change in v5.10.5 #10827
11+
* docs: remove duplicate `path` in docs #11020 [ItWorksOnMyMachine](https://github.com/ItWorksOnMyMachine)
12+
* docs: fix typo in populate docs #11015 [gavi-shandler](https://github.com/gavi-shandler)
13+
* docs: fix typo in model.js #10982 [eltociear](https://github.com/eltociear)
14+
115
6.0.13 / 2021-11-15
216
===================
317
* fix(document): allows validating doc again if pre validate errors out #10830

docs/migrating_to_6.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration
3636
* [Removed Validator `isAsync`](#removed-validator-isasync)
3737
* [Removed `safe`](#removed-safe)
3838
* [SchemaType `set` parameters now use `priorValue` as the second parameter instead of `self`](#schematype-set-parameters)
39+
* [`toObject()` and `toJSON()` Use Nested Schema `minimize`](#toobject-and-tojson-use-nested-schema-minimize)
3940
* [TypeScript changes](#typescript-changes)
4041

4142
<h3 id="version-requirements"><a href="#version-requirements">Version Requirements</a></h3>
@@ -359,6 +360,37 @@ const user = new User({ name: 'Robert Martin' });
359360
console.log(user.name); // 'robert martin'
360361
```
361362

363+
<h3 id="toobject-and-tojson-use-nested-schema-minimize"><a href="#toobject-and-tojson-use-nested-schema-minimize">`toObject()` and `toJSON()` Use Nested Schema `minimize`</a></h3>
364+
365+
This change was technically released with 5.10.5, but [caused issues for users migrating from 5.9.x to 6.x](https://github.com/Automattic/mongoose/issues/10827).
366+
In Mongoose `< 5.10.5`, `toObject()` and `toJSON()` would use the top-level schema's `minimize` option by default.
367+
368+
```javascript
369+
const child = new Schema({ thing: Schema.Types.Mixed });
370+
const parent = new Schema({ child }, { minimize: false });
371+
const Parent = model('Parent', parent);
372+
const p = new Parent({ child: { thing: {} } });
373+
374+
// In v5.10.4, would contain `child.thing` because `toObject()` uses `parent` schema's `minimize` option
375+
// In `>= 5.10.5`, `child.thing` is omitted because `child` schema has `minimize: true`
376+
console.log(p.toObject());
377+
```
378+
379+
As a workaround, you can either explicitly pass `minimize` to `toObject()` or `toJSON()`:
380+
381+
```javascript
382+
console.log(p.toObject({ minimize: false }));
383+
```
384+
385+
Or define the `child` schema inline (Mongoose 6 only) to inherit the parent's `minimize` option.
386+
387+
```javascript
388+
const parent = new Schema({
389+
// Implicitly creates a new schema with the top-level schema's `minimize` option.
390+
child: { type: { thing: Schema.Types.Mixed } }
391+
}, { minimize: false });
392+
```
393+
362394
## TypeScript changes
363395

364396
The `Schema` class now takes 3 generic params instead of 4. The 3rd generic param, `SchemaDefinitionType`, is now the same as the 1st generic param `DocType`. Replace `new Schema<UserDocument, UserModel, User>(schemaDefinition)` with `new Schema<UserDocument, UserModel>(schemaDefinition)`

docs/populate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ let authors = await Author.
667667

668668
authors = await Author.
669669
find({}).
670-
// Works, foreign field `band` is selected
670+
// Works, foreign field `author` is selected
671671
populate({ path: 'posts', select: 'title author' }).
672672
exec();
673673
```

index.d.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -659,10 +659,10 @@ declare module 'mongoose' {
659659
$parent(): Document | undefined;
660660

661661
/** Populates document references. */
662-
populate<Paths>(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise<this & Paths>;
663-
populate<Paths>(path: string | PopulateOptions | (string | PopulateOptions)[], callback: Callback<this & Paths>): void;
664-
populate<Paths>(path: string, names: string): Promise<this & Paths>;
665-
populate<Paths>(path: string, names: string, callback: Callback<this & Paths>): void;
662+
populate<Paths = {}>(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise<this & Paths>;
663+
populate<Paths = {}>(path: string | PopulateOptions | (string | PopulateOptions)[], callback: Callback<this & Paths>): void;
664+
populate<Paths = {}>(path: string, names: string): Promise<this & Paths>;
665+
populate<Paths = {}>(path: string, names: string, callback: Callback<this & Paths>): void;
666666

667667
/** Gets _id(s) used during population of the given `path`. If the path was not populated, returns `undefined`. */
668668
populated(path: string): any;
@@ -1248,7 +1248,7 @@ declare module 'mongoose' {
12481248
type PostMiddlewareFunction<ThisType, ResType = any> = (this: ThisType, res: ResType, next: (err?: CallbackError) => void) => void | Promise<void>;
12491249
type ErrorHandlingMiddlewareFunction<ThisType, ResType = any> = (this: ThisType, err: NativeError, res: ResType, next: (err?: CallbackError) => void) => void;
12501250

1251-
class Schema<DocType = any, M = Model<DocType, any, any, any>, TInstanceMethods = {}> extends events.EventEmitter {
1251+
class Schema<DocType = any, M = Model<DocType, any, any, any>, TInstanceMethods = any> extends events.EventEmitter {
12521252
/**
12531253
* Create a new schema
12541254
*/
@@ -2364,8 +2364,8 @@ declare module 'mongoose' {
23642364
polygon(path: string, ...coordinatePairs: number[][]): this;
23652365

23662366
/** Specifies paths which should be populated with other documents. */
2367-
populate<Paths>(path: string | any, select?: string | any, model?: string | Model<any, THelpers>, match?: any): QueryWithHelpers<ResultType & Paths, DocType, THelpers, RawDocType>;
2368-
populate<Paths>(options: PopulateOptions | Array<PopulateOptions>): QueryWithHelpers<ResultType & Paths, DocType, THelpers, RawDocType>;
2367+
populate<Paths = {}>(path: string | any, select?: string | any, model?: string | Model<any, THelpers>, match?: any): QueryWithHelpers<ResultType & Paths, DocType, THelpers, RawDocType>;
2368+
populate<Paths = {}>(options: PopulateOptions | Array<PopulateOptions>): QueryWithHelpers<ResultType & Paths, DocType, THelpers, RawDocType>;
23692369

23702370
/** Get/set the current projection (AKA fields). Pass `null` to remove the current projection. */
23712371
projection(fields?: any | null): any;

index.pug

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,9 @@ html(lang='en')
382382
<a rel="sponsored" href="https://www.instafollowers.co/">
383383
<img class="sponsor" src="https://images.opencollective.com/instafollowersco/5c0cddd/logo/256.png" style="height: 100px">
384384
</a>
385+
<a rel="sponsored" href="https://casino.guide/crypto-casinos/">
386+
<img class="sponsor" src="https://casino.guide/wp-content/themes/casinoguide/assets/lotti/en/default/images/img_0.png" style="height: 100px">
387+
</a>
385388
</div>
386389
</div>
387390

lib/document.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2421,7 +2421,11 @@ function _evaluateRequiredFunctions(doc) {
24212421

24222422
if (p != null && typeof p.originalRequiredValue === 'function') {
24232423
doc.$__.cachedRequired = doc.$__.cachedRequired || {};
2424-
doc.$__.cachedRequired[path] = p.originalRequiredValue.call(doc, doc);
2424+
try {
2425+
doc.$__.cachedRequired[path] = p.originalRequiredValue.call(doc, doc);
2426+
} catch (err) {
2427+
doc.invalidate(path, err);
2428+
}
24252429
}
24262430
});
24272431
}

lib/helpers/populate/assignVals.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ module.exports = function assignVals(o) {
7373

7474
return valueFilter(val[0], options, populateOptions, _allIds);
7575
} else if (o.justOne === false && !Array.isArray(val)) {
76-
if (val === null && o.isVirtual) {
77-
return void 0;
78-
}
7976
return valueFilter([val], options, populateOptions, _allIds);
8077
}
8178
return valueFilter(val, options, populateOptions, _allIds);
@@ -133,6 +130,9 @@ module.exports = function assignVals(o) {
133130
docs[i].$populated(_path, o.justOne ? originalIds[0] : originalIds, o.allOptions);
134131
// If virtual populate and doc is already init-ed, need to walk through
135132
// the actual doc to set rather than setting `_doc` directly
133+
if (Array.isArray(valueToSet)) {
134+
valueToSet = valueToSet.map(v => v == null ? void 0 : v);
135+
}
136136
mpath.set(_path, valueToSet, docs[i], void 0, setValue, false);
137137
continue;
138138
}

lib/helpers/populate/getModelsMapForPopulate.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,12 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
305305
}
306306

307307
if (!modelNames) {
308-
return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
308+
// `Model.populate()` on a POJO with no known local model. Default to using the `Model`
309+
if (options._localModel == null) {
310+
modelNames = [model.modelName];
311+
} else {
312+
return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
313+
}
309314
}
310315

311316
if (!Array.isArray(modelNames)) {

lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Mongoose.prototype.driver = driver;
156156
* - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject)
157157
* - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()`
158158
* - 'strict': true by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
159-
* - 'strictQuery': false by default, may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas.
159+
* - 'strictQuery': same value as 'strict' by default (`true`), may be `false`, `true`, or `'throw'`. Sets the default [strictQuery](/docs/guide.html#strictQuery) mode for schemas.
160160
* - 'selectPopulatedPaths': true by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one.
161161
* - 'maxTimeMS': If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query
162162
* - 'autoIndex': true by default. Set to false to disable automatic index creation for all models associated with this Mongoose instance.

lib/model.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,17 @@ Model.prototype.$__handleSave = function(options, callback) {
343343
where[key] = val;
344344
}
345345
}
346-
this.constructor.exists(where, optionsWithCustomValues)
347-
.then((documentExists) => {
346+
this.constructor.exists(where, optionsWithCustomValues).
347+
then((documentExists) => {
348348
if (!documentExists) {
349-
throw new DocumentNotFoundError(this.$__where(), this.constructor.modelName);
349+
const matchedCount = 0;
350+
return callback(null, { $where: where, matchedCount });
350351
}
351352

352-
callback();
353-
})
354-
.catch(callback);
353+
const matchedCount = 1;
354+
callback(null, { $where: where, matchedCount });
355+
}).
356+
catch(callback);
355357
return;
356358
}
357359

@@ -379,7 +381,7 @@ Model.prototype.$__save = function(options, callback) {
379381
if (get(options, 'safe.w') !== 0 && get(options, 'w') !== 0) {
380382
// Skip checking if write succeeded if writeConcern is set to
381383
// unacknowledged writes, because otherwise `numAffected` will always be 0
382-
if (result) {
384+
if (result != null) {
383385
if (Array.isArray(result)) {
384386
numAffected = result.length;
385387
} else if (result.matchedCount != null) {
@@ -916,7 +918,7 @@ Model.prototype.$__where = function _where(where) {
916918
* })
917919
*
918920
*
919-
* As an extra measure of flow control, remove will return a Promise (bound to `fn` if passed) so it could be chained, or hooked to recieve errors
921+
* As an extra measure of flow control, remove will return a Promise (bound to `fn` if passed) so it could be chained, or hooked to receive errors
920922
*
921923
* ####Example:
922924
* product.remove().then(function (product) {

lib/options/SchemaTypeOptions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Object.defineProperty(SchemaTypeOptions.prototype, 'cast', opts);
8282

8383
/**
8484
* If true, attach a required validator to this path, which ensures this path
85-
* path cannot be set to a nullish value. If a function, Mongoose calls the
85+
* cannot be set to a nullish value. If a function, Mongoose calls the
8686
* function and only checks for nullish values if the function returns a truthy value.
8787
*
8888
* @api public

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mongoose",
33
"description": "Mongoose MongoDB ODM",
4-
"version": "6.0.13",
4+
"version": "6.0.14",
55
"author": "Guillermo Rauch <guillermo@learnboost.com>",
66
"keywords": [
77
"mongodb",

test/document.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const DocumentObjectId = mongoose.Types.ObjectId;
2323
const SchemaType = mongoose.SchemaType;
2424
const ValidatorError = SchemaType.ValidatorError;
2525
const ValidationError = mongoose.Document.ValidationError;
26+
const VersionError = mongoose.Error.VersionError;
2627
const MongooseError = mongoose.Error;
2728
const DocumentNotFoundError = mongoose.Error.DocumentNotFoundError;
2829

@@ -1875,6 +1876,21 @@ describe('document', function() {
18751876
assert.equal(err.message, `No document found for query "{ _id: new ObjectId("${person._id}") }" on model "Person"`);
18761877
});
18771878

1879+
it('saving a document when version bump required, throws a VersionError when document is not found (gh-10974)', async function() {
1880+
const personSchema = new Schema({ tags: [String] });
1881+
const Person = db.model('Person', personSchema);
1882+
1883+
const person = await Person.create({ tags: ['tag1', 'tag2'] });
1884+
1885+
await Person.deleteOne({ _id: person._id });
1886+
1887+
person.tags.splice(0, 1);
1888+
1889+
const err = await person.save().then(() => null, err => err);
1890+
assert.ok(err instanceof VersionError);
1891+
assert.equal(err.message, `No matching document found for id "${person._id}" version 0 modifiedPaths "tags"`);
1892+
});
1893+
18781894
it('saving a document with changes, throws an error when document is not found', async function() {
18791895

18801896
const personSchema = new Schema({ name: String });
@@ -10744,6 +10760,7 @@ describe('document', function() {
1074410760
doc.quantity = 26;
1074510761
await doc.save();
1074610762
});
10763+
1074710764
it('ensures that doc.ownerDocument() and doc.parent() by default return this on the root document (gh-10884)', async function() {
1074810765
const userSchema = new mongoose.Schema({
1074910766
name: String,
@@ -10756,4 +10773,20 @@ describe('document', function() {
1075610773
assert.strictEqual(e, e.parent());
1075710774
assert.strictEqual(e, e.ownerDocument());
1075810775
});
10776+
10777+
it('catches errors in `required` functions (gh-10968)', async function() {
10778+
const TestSchema = new Schema({
10779+
url: {
10780+
type: String,
10781+
required: function() {
10782+
throw new Error('oops!');
10783+
}
10784+
}
10785+
});
10786+
const Test = db.model('Test', TestSchema);
10787+
10788+
const err = await Test.create({}).then(() => null, err => err);
10789+
assert.ok(err);
10790+
assert.equal(err.errors['url'].message, 'oops!');
10791+
});
1075910792
});

test/model.populate.test.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4491,7 +4491,6 @@ describe('model: populate:', function() {
44914491
});
44924492

44934493
it('justOne + lean (gh-6234)', async function() {
4494-
44954494
const PersonSchema = new mongoose.Schema({
44964495
name: String,
44974496
band: String
@@ -4530,6 +4529,35 @@ describe('model: populate:', function() {
45304529
assert.equal(res[1].member.name, 'Vince Neil');
45314530
});
45324531

4532+
it('sets empty array if lean with justOne = false and no results (gh-10992)', async function() {
4533+
const PersonSchema = new mongoose.Schema({
4534+
name: String,
4535+
band: String
4536+
});
4537+
4538+
const BandSchema = new mongoose.Schema({
4539+
name: String
4540+
});
4541+
4542+
BandSchema.virtual('members', {
4543+
ref: 'Person',
4544+
localField: 'name',
4545+
foreignField: 'band',
4546+
justOne: false
4547+
});
4548+
4549+
db.model('Person', PersonSchema);
4550+
const Band = db.model('Test', BandSchema);
4551+
4552+
await Band.create({ name: 'Guns N\' Roses' });
4553+
4554+
const res = await Band.find().populate('members').lean();
4555+
4556+
assert.equal(res.length, 1);
4557+
assert.equal(res[0].name, 'Guns N\' Roses');
4558+
assert.deepStrictEqual(res[0].members, []);
4559+
});
4560+
45334561
it('justOne underneath array (gh-6867)', async function() {
45344562

45354563
const ReportItemSchema = new Schema({
@@ -6570,7 +6598,6 @@ describe('model: populate:', function() {
65706598
});
65716599

65726600
it('virtual populate with embedded discriminators (gh-6273)', async function() {
6573-
65746601
// Generate Users Model
65756602
const userSchema = new Schema({ employeeId: Number, name: String });
65766603
const UserModel = db.model('User', userSchema);
@@ -10447,4 +10474,37 @@ describe('model: populate:', function() {
1044710474
const doc = await BlogPost.findOne().populate('author');
1044810475
assert.equal(doc.author.email, 'test@gmail.com');
1044910476
});
10477+
10478+
it('uses `Model` by default when doing `Model.populate()` on a POJO (gh-10978)', async function() {
10479+
const UserSchema = new Schema({
10480+
name: { type: String, default: '' }
10481+
});
10482+
10483+
const TestSchema = new Schema({
10484+
users: [{ user: { type: 'ObjectId', ref: 'User' } }]
10485+
});
10486+
10487+
const User = db.model('User', UserSchema);
10488+
const Test = db.model('Test', TestSchema);
10489+
10490+
const users = await User.create([{ name: 'user-name' }, { name: 'user-name-2' }]);
10491+
await Test.create([{ users: [{ user: users[0]._id }, { user: users[1]._id }] }]);
10492+
10493+
const found = await Test.aggregate([
10494+
{
10495+
$project: {
10496+
users: '$users'
10497+
}
10498+
},
10499+
{ $unwind: '$users' },
10500+
{ $sort: { 'users.name': 1 } }
10501+
]);
10502+
10503+
const _users = found.reduce((users, cur) => [...users, cur.users], []);
10504+
10505+
await User.populate(_users, { path: 'user' });
10506+
assert.equal(_users.length, 2);
10507+
assert.equal(_users[0].user.name, 'user-name');
10508+
assert.equal(_users[1].user.name, 'user-name-2');
10509+
});
1045010510
});

0 commit comments

Comments
 (0)