Skip to content

Commit

Permalink
Merge branch 'master' into vkarpov15/gh-13211
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 authored Aug 20, 2024
2 parents edd94fc + 3ee2de5 commit f905ac6
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 14 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
7.8.1 / 2024-08-19
==================
* fix(query): handle casting $switch in $expr #14761
* docs(mongoose): remove out-of-date callback-based example for mongoose.connect() #14811 #14810

8.5.3 / 2024-08-13
==================
* fix(document): call required functions on subdocuments underneath nested paths with correct context #14801 #14788
Expand All @@ -20,6 +25,11 @@
* types: allow calling SchemaType.cast() without parent and init parameters #14756 #14748 #9076
* docs: fix a wrong example in v6 migration guide #14758 [abdelrahman-elkady](https://github.com/abdelrahman-elkady)

7.8.0 / 2024-07-23
==================
* feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889
* types(query): fix usage of "RawDocType" where "DocType" should be passed #14737 [hasezoey](https://github.com/hasezoey)

8.5.1 / 2024-07-12
==================
* perf(model): performance improvements for insertMany() #14724
Expand Down Expand Up @@ -59,6 +69,10 @@
* types: add $documents pipeline stage and fix $unionWith type #14666 [nick-statsig](https://github.com/nick-statsig)
* docs(findoneandupdate): improve example that shows findOneAndUpdate() returning doc before updates were applied #14671 #14670

7.7.0 / 2024-06-18
==================
* feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410

8.4.3 / 2024-06-17
==================
* fix: remove 0x flamegraph files from release
Expand All @@ -72,13 +86,28 @@
* types: avoid inferring Boolean, Buffer, ObjectId as Date in schema definitions under certain circumstances #14667 #14630
* docs: add note about parallelism in transations #14647 [fiws](https://github.com/fiws)

6.13.0 / 2024-06-06
===================
* feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410

7.6.13 / 2024-06-05
===================
* fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments #14614 #14610
* types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601
* docs(migrating_to_7): add id setter to Mongoose 7 migration guide #14645 #13672

8.4.1 / 2024-05-31
==================
* fix: pass options to clone instead of get in applyVirtuals #14606 #14543 [andrews05](https://github.com/andrews05)
* fix(document): fire pre validate hooks on 5 level deep single nested subdoc when modifying after save() #14604 #14591
* fix: ensure buildBulkWriteOperations target shard if shardKey is set #14622 #14621 [matlpriceshape](https://github.com/matlpriceshape)
* types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601

6.12.9 / 2024-05-24
===================
* fix(cast): cast $comment to string in query filters #14590 #14576
* types(model): allow passing strict type checking override to create() #14571 #14548

7.6.12 / 2024-05-21
===================
* fix(array): avoid converting to $set when calling pull() on an element in the middle of the array #14531 #14502
Expand Down Expand Up @@ -559,6 +588,7 @@
==================
* perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
* feat: upgrade to MongoDB Node.js driver 5.7.0 #13591
* feat: add `id` setter which allows modifying `_id` by setting `id` (Note this change was reverted in Mongoose 8) #13517
* feat: support generating custom cast error message with a function #13608 #3162
* feat(query): support MongoDB driver's includeResultMetadata option for findOneAndUpdate #13584 #13539
* feat(connection): add Connection.prototype.removeDb() for removing a related connection #13580 #11821
Expand Down
26 changes: 26 additions & 0 deletions docs/migrating_to_7.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration
* [Dropped callback support](#dropped-callback-support)
* [Removed `update()`](#removed-update)
* [ObjectId requires `new`](#objectid-requires-new)
* [`id` setter](#id-setter)
* [Discriminator schemas use base schema options by default](#discriminator-schemas-use-base-schema-options-by-default)
* [Removed `castForQueryWrapper()`, updated `castForQuery()` signature](#removed-castforquerywrapper)
* [Copy schema options in `Schema.prototype.add()`](#copy-schema-options-in-schema-prototype-add)
Expand Down Expand Up @@ -199,6 +200,31 @@ In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tut
const oid = new mongoose.Types.ObjectId('0'.repeat(24));
```

## `id` Setter

Starting in Mongoose 7.4, Mongoose's built-in `id` virtual (which stores the document's `_id` as a string) has a setter which allows modifying the document's `_id` property via `id`.

```javascript
const doc = await TestModel.findOne();

doc.id = '000000000000000000000000';
doc._id; // ObjectId('000000000000000000000000')
```

This can cause surprising behavior if you create a `new TestModel(obj)` where `obj` contains both an `id` and an `_id`, or if you use `doc.set()`

```javascript
// Because `id` is after `_id`, the `id` will overwrite the `_id`
const doc = new TestModel({
_id: '000000000000000000000000',
id: '111111111111111111111111'
});

doc._id; // ObjectId('111111111111111111111111')
```

[The `id` setter was later removed in Mongoose 8](/docs/migrating_to_8.html#removed-id-setter) due to compatibility issues.

## Discriminator schemas use base schema options by default {#discriminator-schemas-use-base-schema-options-by-default}

When you use `Model.discriminator()`, Mongoose will now use the discriminator base schema's options by default.
Expand Down
4 changes: 4 additions & 0 deletions lib/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const CastError = require('./error/cast');
const StrictModeError = require('./error/strict');
const Types = require('./schema/index');
const cast$expr = require('./helpers/query/cast$expr');
const castString = require('./cast/string');
const castTextSearch = require('./schema/operators/text');
const get = require('./helpers/get');
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
Expand Down Expand Up @@ -95,6 +96,9 @@ module.exports = function cast(schema, obj, options, context) {
val = cast(schema, val, options, context);
} else if (path === '$text') {
val = castTextSearch(val, path);
} else if (path === '$comment' && !schema.paths.hasOwnProperty('$comment')) {
val = castString(val, path);
obj[path] = val;
} else {
if (!schema) {
// no casting for Mixed types
Expand Down
2 changes: 1 addition & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const Document = require('./document');
const DocumentNotFoundError = require('./error/notFound');
const EventEmitter = require('events').EventEmitter;
const Kareem = require('kareem');
const MongooseBulkWriteError = require('./error/bulkWriteError');
const MongooseError = require('./error/index');
const ObjectParameterError = require('./error/objectParameter');
const OverwriteModelError = require('./error/overwriteModel');
Expand Down Expand Up @@ -62,7 +63,6 @@ const setDottedPath = require('./helpers/path/setDottedPath');
const STATES = require('./connectionState');
const util = require('util');
const utils = require('./utils');
const MongooseBulkWriteError = require('./error/bulkWriteError');
const minimize = require('./helpers/minimize');

const modelCollectionSymbol = Symbol('mongoose#Model#collection');
Expand Down
9 changes: 4 additions & 5 deletions lib/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ Mongoose.prototype.get = Mongoose.prototype.set;
*
* // initialize now, connect later
* db = mongoose.createConnection();
* db.openUri('127.0.0.1', 'database', port, [opts]);
* await db.openUri('mongodb://127.0.0.1:27017/database');
*
* @param {String} uri mongodb URI to connect to
* @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
Expand Down Expand Up @@ -406,11 +406,10 @@ Mongoose.prototype.createConnection = function(uri, options) {
* // with options
* mongoose.connect(uri, options);
*
* // optional callback that gets fired when initial connection completed
* // Using `await` throws "MongooseServerSelectionError: Server selection timed out after 30000 ms"
* // if Mongoose can't connect.
* const uri = 'mongodb://nonexistent.domain:27000';
* mongoose.connect(uri, function(error) {
* // if error is truthy, the initial connection failed.
* })
* await mongoose.connect(uri);
*
* @param {String} uri mongodb URI to connect to
* @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below.
Expand Down
12 changes: 8 additions & 4 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -2446,19 +2446,23 @@ Query.prototype.merge = function(source) {
}

opts.omit = {};
if (source.$and) {
if (Array.isArray(source.$and)) {
opts.omit['$and'] = true;
if (!this._conditions) {
this._conditions = {};
}
this._conditions.$and = (this._conditions.$and || []).concat(source.$and);
this._conditions.$and = (this._conditions.$and || []).concat(
source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el)
);
}
if (source.$or) {
if (Array.isArray(source.$or)) {
opts.omit['$or'] = true;
if (!this._conditions) {
this._conditions = {};
}
this._conditions.$or = (this._conditions.$or || []).concat(source.$or);
this._conditions.$or = (this._conditions.$or || []).concat(
source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el)
);
}

// plain object
Expand Down
2 changes: 1 addition & 1 deletion lib/schema/operators/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const castString = require('../../cast/string');
* @api private
*/

module.exports = function(val, path) {
module.exports = function castTextSearch(val, path) {
if (val == null || typeof val !== 'object') {
throw new CastError('$text', val, path);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ exports.merge = function merge(to, from, options, path) {
to[key] = from[key];
}
}

return to;
};

/**
Expand Down
27 changes: 27 additions & 0 deletions test/cast.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,33 @@ describe('cast: ', function() {
});
});

it('casts $comment (gh-14576)', function() {
const schema = new Schema({ name: String });

let res = cast(schema, {
$comment: 'test'
});
assert.deepStrictEqual(res, { $comment: 'test' });

res = cast(schema, {
$comment: 42
});
assert.deepStrictEqual(res, { $comment: '42' });

assert.throws(
() => cast(schema, {
$comment: { name: 'taco' }
}),
/\$comment/
);

const schema2 = new Schema({ $comment: Number });
res = cast(schema2, {
$comment: 42
});
assert.deepStrictEqual(res, { $comment: 42 });
});

it('avoids setting stripped out nested schema values to undefined (gh-11291)', function() {
const nested = new Schema({}, {
id: false,
Expand Down
104 changes: 104 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4507,6 +4507,71 @@ describe('Model', function() {
assert.strictEqual(doc.name, 'test');
assert.deepStrictEqual(doc.ids, ['1', '2', '3']);
});

it('throwOnValidationError (gh-14572) (gh-13256)', async function() {
const schema = new Schema({
num: Number
});

const M = db.model('Test', schema);

const ops = [
{
insertOne: {
document: {
num: 'not a number'
}
}
}
];

const err = await M.bulkWrite(
ops,
{ ordered: false, throwOnValidationError: true }
).then(() => null, err => err);
assert.ok(err);
assert.equal(err.name, 'MongooseBulkWriteError');
assert.equal(err.validationErrors[0].errors['num'].name, 'CastError');
});

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() {
Expand Down Expand Up @@ -7575,6 +7640,45 @@ describe('Model', function() {

assert.strictEqual(doc.__v, 0);
});

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);

let 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');

let docs = await User.find();
assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);

err = await User.insertMany([
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');

docs = await User.find();
assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]);
});
});


Expand Down
15 changes: 15 additions & 0 deletions test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4314,4 +4314,19 @@ describe('Query', function() {

assert.deepStrictEqual(adultQuery, { age: { $gte: 18 } });
});

it('avoids mutating $or, $and elements when casting (gh-14610)', async function() {
const personSchema = new mongoose.Schema({
name: String,
age: Number
});
const Person = db.model('Person', personSchema);

const filter = [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }];
await Person.find({ $or: filter });
assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]);

await Person.find({ $and: filter });
assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]);
});
});
Loading

0 comments on commit f905ac6

Please sign in to comment.