Skip to content

Commit

Permalink
Merge branch 'master' into 8.2
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Feb 20, 2024
2 parents db247da + 7732ce2 commit 5d7178c
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 18 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
8.1.3 / 2024-02-16
==================
* fix: avoid corrupting $set-ed arrays when transaction error occurs #14346 #14340
* fix(populate): handle ref() functions that return a model instance #14343 #14249
* fix: insert version key when using insertMany even if `toObject.versionKey` set to false #14344
* fix(cursor): make aggregation cursor support transform option to match query cursor #14348 #14331
* docs(document): clarify that transform function option applies to subdocs #13757

8.1.2 / 2024-02-08
==================
* fix: include virtuals in document array toString() output if toObject.virtuals set #14335 #14315
* fix(document): handle setting nested path to spread doc with extra properties #14287 #14269
* fix(populate): call setter on virtual populated path with populated doc instead of undefined #14314
* fix(QueryCursor): remove callback parameter of AggregationCursor and QueryCursor #14299 [DevooKim](https://github.com/DevooKim)
* types: add typescript support for arbitrary fields for the options parameter of Model functions which are of type MongooseQueryOptions #14342 #14341 [FaizBShah](https://github.com/FaizBShah)
* types(model): correct return type for findOneAndUpdate with includeResultMetadata and lean set #14336 #14303
* types(connection): add type definition for `createCollections()` #14295 #14279
* docs(timestamps): clarify that replaceOne() and findOneAndReplace() overwrite timestamps #14337 #14309

8.1.1 / 2024-01-24
==================
* fix(model): throw readable error when calling Model() with a string instead of model() #14288 #14281
Expand Down
8 changes: 8 additions & 0 deletions lib/cursor/aggregationCursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,20 @@ util.inherits(AggregationCursor, Readable);
function _init(model, c, agg) {
if (!model.collection.buffer) {
model.hooks.execPre('aggregate', agg, function() {
if (typeof agg.options?.cursor?.transform === 'function') {
c._transforms.push(agg.options.cursor.transform);
}

c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
} else {
model.collection.emitter.once('queue', function() {
model.hooks.execPre('aggregate', agg, function() {
if (typeof agg.options?.cursor?.transform === 'function') {
c._transforms.push(agg.options.cursor.transform);
}

c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
c.emit('cursor', c.cursor);
});
Expand Down
23 changes: 17 additions & 6 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -3897,14 +3897,24 @@ Document.prototype.$toObject = function(options, json) {
*
* _Note: if a transform function returns `undefined`, the return value will be ignored._
*
* Transformations may also be applied inline, overridding any transform set in the options:
* Transformations may also be applied inline, overridding any transform set in the schema options.
* Any transform function specified in `toObject` options also propagates to any subdocuments.
*
* function xform (doc, ret, options) {
* return { inline: ret.name, custom: true }
* function deleteId(doc, ret, options) {
* delete ret._id;
* return ret;
* }
*
* // pass the transform as an inline option
* doc.toObject({ transform: xform }); // { inline: 'Wreck-it Ralph', custom: true }
* const schema = mongoose.Schema({ name: String, docArr: [{ name: String }] });
* const TestModel = mongoose.model('Test', schema);
*
* const doc = new TestModel({ name: 'test', docArr: [{ name: 'test' }] });
*
* // pass the transform as an inline option. Deletes `_id` property
* // from both the top-level document and the subdocument.
* const obj = doc.toObject({ transform: deleteId });
* obj._id; // undefined
* obj.docArr[0]._id; // undefined
*
* If you want to skip transformations, use `transform: false`:
*
Expand Down Expand Up @@ -4283,7 +4293,8 @@ Document.prototype.inspect = function(options) {
opts = options;
opts.minimize = false;
}
const ret = this.toObject(opts);

const ret = arguments.length > 0 ? this.toObject(opts) : this.toObject();

if (ret == null) {
// If `toObject()` returns null, `this` is still an object, so if `inspect()`
Expand Down
3 changes: 2 additions & 1 deletion lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re

let k = modelNames.length;
while (k--) {
const modelName = modelNames[k];
let modelName = modelNames[k];
if (modelName == null) {
continue;
}
Expand All @@ -504,6 +504,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
Model = options.model;
} else if (modelName[modelSymbol]) {
Model = modelName;
modelName = Model.modelName;
} else {
try {
Model = _getModelFromConn(connection, modelName);
Expand Down
3 changes: 2 additions & 1 deletion lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ exports.internalToObjectOptions = {
_skipDepopulateTopLevel: true,
depopulate: true,
flattenDecimals: false,
useProjection: false
useProjection: false,
versionKey: true
};
2 changes: 1 addition & 1 deletion lib/plugins/trackTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function mergeAtomics(destination, source) {
destination.$addToSet = (destination.$addToSet || []).concat(source.$addToSet);
}
if (source.$set != null) {
destination.$set = Object.assign(destination.$set || {}, source.$set);
destination.$set = Array.isArray(source.$set) ? [...source.$set] : Object.assign({}, source.$set);
}

return destination;
Expand Down
12 changes: 12 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2144,6 +2144,18 @@ Schema.prototype.set = function(key, value, tags) {
if (key === 'strictQuery') {
_propagateOptionsToImplicitlyCreatedSchemas(this, { strictQuery: value });
}
if (key === 'toObject') {
value = { ...value };
// Avoid propagating transform to implicitly created schemas re: gh-3279
delete value.transform;
_propagateOptionsToImplicitlyCreatedSchemas(this, { toObject: value });
}
if (key === 'toJSON') {
value = { ...value };
// Avoid propagating transform to implicitly created schemas re: gh-3279
delete value.transform;
_propagateOptionsToImplicitlyCreatedSchemas(this, { toJSON: value });
}

return this;
};
Expand Down
11 changes: 11 additions & 0 deletions lib/types/documentArray/methods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
const documentArrayParent = require('../../../helpers/symbols').documentArrayParent;

const _baseToString = Array.prototype.toString;

const methods = {
/*!
* ignore
Expand All @@ -22,6 +24,15 @@ const methods = {
return this.toObject(internalToObjectOptions);
},

toString() {
return _baseToString.call(this.__array.map(subdoc => {
if (subdoc != null && subdoc.$__ != null) {
return subdoc.toString();
}
return subdoc;
}));
},

/*!
* ignore
*/
Expand Down
6 changes: 1 addition & 5 deletions lib/types/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,7 @@ Subdocument.prototype.populate = function() {
*/

Subdocument.prototype.inspect = function() {
return this.toObject({
transform: false,
virtuals: false,
flattenDecimals: false
});
return this.toObject();
};

if (util.inspect.custom) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
"version": "8.1.1",
"version": "8.1.3",
"author": "Guillermo Rauch <guillermo@learnboost.com>",
"keywords": [
"mongodb",
Expand Down
3 changes: 1 addition & 2 deletions scripts/loadSponsorData.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ async function run() {

const OpenCollectiveSponsor = mongoose.model('OpenCollectiveSponsor', mongoose.Schema({
openCollectiveId: {
type: Number,
required: true
type: Number
},
website: {
type: String,
Expand Down
27 changes: 27 additions & 0 deletions test/aggregate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const start = require('./common');

const assert = require('assert');
const stream = require('stream');

const Aggregate = require('../lib/aggregate');

Expand Down Expand Up @@ -1215,6 +1216,32 @@ describe('aggregate: ', function() {
assert.equal(res[1].test, 'a test');
});

it('cursor supports transform option (gh-14331)', async function() {
const mySchema = new Schema({ name: String });
const Test = db.model('Test', mySchema);

await Test.deleteMany({});
await Test.create([{ name: 'Apple' }, { name: 'Apple' }]);

let resolve;
const waitForStream = new Promise(innerResolve => {
resolve = innerResolve;
});
const otherStream = new stream.Writable({
write(chunk, encoding, callback) {
resolve(chunk.toString());
callback();
}
});

await Test.
aggregate([{ $match: { name: 'Apple' } }]).
cursor({ transform: JSON.stringify }).
pipe(otherStream);
const streamValue = await waitForStream;
assert.ok(streamValue.includes('"name":"Apple"'), streamValue);
});

describe('Mongo 3.6 options', function() {
before(async function() {
await onlyTestAtOrAbove('3.6', this);
Expand Down
56 changes: 56 additions & 0 deletions test/docs/transactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,62 @@ describe('transactions', function() {
assert.equal(docs[0].name, 'test');
});

it('handles resetting array state with $set atomic (gh-13698)', async function() {
db.deleteModel(/Test/);
const subItemSchema = new mongoose.Schema(
{
name: { type: String, required: true }
},
{ _id: false }
);

const itemSchema = new mongoose.Schema(
{
name: { type: String, required: true },
subItems: { type: [subItemSchema], required: true }
},
{ _id: false }
);

const schema = new mongoose.Schema({
items: { type: [itemSchema], required: true }
});

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

const { _id } = await Test.create({
items: [
{ name: 'test1', subItems: [{ name: 'x1' }] },
{ name: 'test2', subItems: [{ name: 'x2' }] }
]
});

const doc = await Test.findById(_id).orFail();
let attempt = 0;

await db.transaction(async(session) => {
await doc.save({ session });

if (attempt === 0) {
attempt += 1;
throw new mongoose.mongo.MongoServerError({
message: 'Test transient transaction failures & retries',
errorLabels: [mongoose.mongo.MongoErrorLabel.TransientTransactionError]
});
}
});

const { items } = await Test.findById(_id).orFail();
assert.ok(Array.isArray(items));
assert.equal(items.length, 2);
assert.equal(items[0].name, 'test1');
assert.equal(items[0].subItems.length, 1);
assert.equal(items[0].subItems[0].name, 'x1');
assert.equal(items[1].name, 'test2');
assert.equal(items[1].subItems.length, 1);
assert.equal(items[1].subItems[0].name, 'x2');
});

it('transaction() retains modified status for documents created outside of the transaction then modified inside the transaction (gh-13973)', async function() {
db.deleteModel(/Test/);
const Test = db.model('Test', Schema({ status: String }));
Expand Down
19 changes: 19 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12953,6 +12953,25 @@ describe('document', function() {
};
assert.equal(person.address.zip, 54321);
});

it('includes virtuals in doc array toString() output if virtuals enabled on toObject (gh-14315)', function() {
const schema = new Schema({
docArr: [{ childId: mongoose.ObjectId }]
});
schema.virtual('docArr.child', { ref: 'Child', localField: 'docArr.childId', foreignField: '_id' });
schema.set('toObject', { virtuals: true });
schema.set('toJSON', { virtuals: true });
const Test = db.model('Test', schema);
const Child = db.model('Child', new Schema({
name: String
}));

const child = new Child({ name: 'test child' });
const doc = new Test({ docArr: [{ childId: child._id }] });
doc.docArr[0].child = child;
assert.ok(doc.docArr.toString().includes('child'), doc.docArr.toString());
assert.ok(doc.docArr.toString().includes('test child'), doc.docArr.toString());
});
});

describe('Check if instance function that is supplied in schema option is availabe', function() {
Expand Down
46 changes: 46 additions & 0 deletions test/model.populate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10922,4 +10922,50 @@ describe('model: populate:', function() {
assert.equal(personFromDb.userType, 'User');
assert.equal(personFromDb.userId.toHexString(), user._id.toHexString());
});

it('handles ref() function that returns a model (gh-14249)', async function() {
const aSchema = new Schema({
name: String
});

const bSchema = new Schema({
name: String
});

const CategoryAModel = db.model('Test', aSchema);
const CategoryBModel = db.model('Test1', bSchema);

const testSchema = new Schema({
category: String,
subdoc: {
type: Schema.Types.ObjectId,
ref: function() {
return this.category === 'catA' ? CategoryAModel : CategoryBModel;
}
}
});

const parentSchema = new Schema({
name: String,
children: [testSchema]
});
const Parent = db.model('Parent', parentSchema);

const A = await CategoryAModel.create({
name: 'A'
});
const B = await CategoryBModel.create({
name: 'B'
});

const doc = await Parent.create({
name: 'Parent',
children: [{ category: 'catA', subdoc: A._id }, { category: 'catB', subdoc: B._id }]
});

const res = await Parent.findById(doc._id).populate('children.subdoc');
assert.equal(res.children.length, 2);
assert.equal(res.children[0].subdoc.name, 'A');
assert.equal(res.children[1].subdoc.name, 'B');
});
});
15 changes: 15 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7315,6 +7315,21 @@ describe('Model', function() {
TestModel.recompileSchema();
assert.equal(doc.myVirtual, 'Hello from myVirtual');
});

it('inserts versionKey even if schema has `toObject.versionKey` set to false (gh-14344)', async function() {
const schema = new mongoose.Schema(
{ name: String },
{ versionKey: '__v', toObject: { versionKey: false } }
);

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

await Model.insertMany([{ name: 'x' }]);

const doc = await Model.findOne();

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


Expand Down
Loading

0 comments on commit 5d7178c

Please sign in to comment.