Skip to content

Commit c48564d

Browse files
authored
Merge pull request Automattic#15270 from Automattic/8.11
8.11
2 parents b22e9a8 + 00ca5c8 commit c48564d

File tree

16 files changed

+359
-65
lines changed

16 files changed

+359
-65
lines changed

lib/cast/bigint.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
const assert = require('assert');
43
const { Long } = require('bson');
54

65
/**
@@ -13,6 +12,10 @@ const { Long } = require('bson');
1312
* @api private
1413
*/
1514

15+
const MAX_BIGINT = 9223372036854775807n;
16+
const MIN_BIGINT = -9223372036854775808n;
17+
const ERROR_MESSAGE = `Mongoose only supports BigInts between ${MIN_BIGINT} and ${MAX_BIGINT} because MongoDB does not support arbitrary precision integers`;
18+
1619
module.exports = function castBigInt(val) {
1720
if (val == null) {
1821
return val;
@@ -21,6 +24,9 @@ module.exports = function castBigInt(val) {
2124
return null;
2225
}
2326
if (typeof val === 'bigint') {
27+
if (val > MAX_BIGINT || val < MIN_BIGINT) {
28+
throw new Error(ERROR_MESSAGE);
29+
}
2430
return val;
2531
}
2632

@@ -29,8 +35,12 @@ module.exports = function castBigInt(val) {
2935
}
3036

3137
if (typeof val === 'string' || typeof val === 'number') {
32-
return BigInt(val);
38+
val = BigInt(val);
39+
if (val > MAX_BIGINT || val < MIN_BIGINT) {
40+
throw new Error(ERROR_MESSAGE);
41+
}
42+
return val;
3343
}
3444

35-
assert.ok(false);
45+
throw new Error(`Cannot convert value to BigInt: "${val}"`);
3646
};

lib/document.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3836,15 +3836,39 @@ Document.prototype.$toObject = function(options, json) {
38363836
// Parent options should only bubble down for subdocuments, not populated docs
38373837
options._parentOptions = this.$isSubdocument ? options : null;
38383838

3839-
// remember the root transform function
3840-
// to save it from being overwritten by sub-transform functions
3841-
// const originalTransform = options.transform;
3839+
const schemaFieldsOnly = options._calledWithOptions.schemaFieldsOnly
3840+
?? options.schemaFieldsOnly
3841+
?? defaultOptions.schemaFieldsOnly
3842+
?? false;
38423843

38433844
let ret;
38443845
if (hasOnlyPrimitiveValues && !options.flattenObjectIds) {
38453846
// Fast path: if we don't have any nested objects or arrays, we only need a
38463847
// shallow clone.
3847-
ret = this.$__toObjectShallow();
3848+
ret = this.$__toObjectShallow(schemaFieldsOnly);
3849+
} else if (schemaFieldsOnly) {
3850+
ret = {};
3851+
for (const path of Object.keys(this.$__schema.paths)) {
3852+
const value = this.$__getValue(path);
3853+
if (value === undefined) {
3854+
continue;
3855+
}
3856+
let pathToSet = path;
3857+
let objToSet = ret;
3858+
if (path.indexOf('.') !== -1) {
3859+
const segments = path.split('.');
3860+
pathToSet = segments[segments.length - 1];
3861+
for (let i = 0; i < segments.length - 1; ++i) {
3862+
objToSet[segments[i]] = objToSet[segments[i]] ?? {};
3863+
objToSet = objToSet[segments[i]];
3864+
}
3865+
}
3866+
if (value === null) {
3867+
objToSet[pathToSet] = null;
3868+
continue;
3869+
}
3870+
objToSet[pathToSet] = clone(value, options);
3871+
}
38483872
} else {
38493873
ret = clone(this._doc, options) || {};
38503874
}
@@ -3910,10 +3934,12 @@ Document.prototype.$toObject = function(options, json) {
39103934
* Internal shallow clone alternative to `$toObject()`: much faster, no options processing
39113935
*/
39123936

3913-
Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
3937+
Document.prototype.$__toObjectShallow = function $__toObjectShallow(schemaFieldsOnly) {
39143938
const ret = {};
39153939
if (this._doc != null) {
3916-
for (const key of Object.keys(this._doc)) {
3940+
const keys = schemaFieldsOnly ? Object.keys(this.$__schema.paths) : Object.keys(this._doc);
3941+
for (const key of keys) {
3942+
// Safe to do this even in the schemaFieldsOnly case because we assume there's no nested paths
39173943
const value = this._doc[key];
39183944
if (value instanceof Date) {
39193945
ret[key] = new Date(value);
@@ -4066,6 +4092,7 @@ Document.prototype.$__toObjectShallow = function $__toObjectShallow() {
40664092
* @param {Boolean} [options.flattenMaps=false] if true, convert Maps to POJOs. Useful if you want to `JSON.stringify()` the result of `toObject()`.
40674093
* @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
40684094
* @param {Boolean} [options.useProjection=false] - If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema.
4095+
* @param {Boolean} [options.schemaFieldsOnly=false] - If true, the resulting object will only have fields that are defined in the document's schema. By default, `toObject()` returns all fields in the underlying document from MongoDB, including ones that are not listed in the schema.
40694096
* @return {Object} document as a plain old JavaScript object (POJO). This object may contain ObjectIds, Maps, Dates, mongodb.Binary, Buffers, and other non-POJO values.
40704097
* @see mongodb.Binary https://mongodb.github.io/node-mongodb-native/4.9/classes/Binary.html
40714098
* @api public
@@ -4336,6 +4363,7 @@ function omitDeselectedFields(self, json) {
43364363
* @param {Object} options
43374364
* @param {Boolean} [options.flattenMaps=true] if true, convert Maps to [POJOs](https://masteringjs.io/tutorials/fundamentals/pojo). Useful if you want to `JSON.stringify()` the result.
43384365
* @param {Boolean} [options.flattenObjectIds=false] if true, convert any ObjectIds in the result to 24 character hex strings.
4366+
* @param {Boolean} [options.schemaFieldsOnly=false] - If true, the resulting object will only have fields that are defined in the document's schema. By default, `toJSON()` returns all fields in the underlying document from MongoDB, including ones that are not listed in the schema.
43394367
* @return {Object}
43404368
* @see Document#toObject https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject()
43414369
* @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
@@ -4506,6 +4534,8 @@ Document.prototype.equals = function(doc) {
45064534
* @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
45074535
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
45084536
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
4537+
* @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
4538+
* @param {Boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. We recommend setting this option to `true` if using transactions, especially if also populating multiple paths or paths with multiple models. MongoDB server does **not** support multiple operations in parallel on a single transaction.
45094539
* @param {Function} [callback] Callback
45104540
* @see population https://mongoosejs.com/docs/populate.html
45114541
* @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
@@ -4532,6 +4562,7 @@ Document.prototype.populate = async function populate() {
45324562
}
45334563

45344564
const paths = utils.object.vals(pop);
4565+
45354566
let topLevelModel = this.constructor;
45364567
if (this.$__isNested) {
45374568
topLevelModel = this.$__[scopeSymbol].constructor;

lib/model.js

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3104,11 +3104,9 @@ Model.$__insertMany = function(arr, options, callback) {
31043104
const res = {
31053105
acknowledged: true,
31063106
insertedCount: 0,
3107-
insertedIds: {},
3108-
mongoose: {
3109-
validationErrors: validationErrors
3110-
}
3107+
insertedIds: {}
31113108
};
3109+
decorateBulkWriteResult(res, validationErrors, validationErrors);
31123110
return callback(null, res);
31133111
}
31143112
callback(null, []);
@@ -3161,10 +3159,7 @@ Model.$__insertMany = function(arr, options, callback) {
31613159

31623160
// Decorate with mongoose validation errors in case of unordered,
31633161
// because then still do `insertMany()`
3164-
res.mongoose = {
3165-
validationErrors: validationErrors,
3166-
results: results
3167-
};
3162+
decorateBulkWriteResult(res, validationErrors, results);
31683163
}
31693164
return callback(null, res);
31703165
}
@@ -3198,10 +3193,7 @@ Model.$__insertMany = function(arr, options, callback) {
31983193
if (error.writeErrors != null) {
31993194
for (let i = 0; i < error.writeErrors.length; ++i) {
32003195
const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index);
3201-
error.writeErrors[i] = {
3202-
...error.writeErrors[i],
3203-
index: originalIndex
3204-
};
3196+
error.writeErrors[i] = { ...error.writeErrors[i], index: originalIndex };
32053197
if (!ordered) {
32063198
results[originalIndex] = error.writeErrors[i];
32073199
}
@@ -3245,10 +3237,7 @@ Model.$__insertMany = function(arr, options, callback) {
32453237
});
32463238

32473239
if (rawResult && ordered === false) {
3248-
error.mongoose = {
3249-
validationErrors: validationErrors,
3250-
results: results
3251-
};
3240+
decorateBulkWriteResult(error, validationErrors, results);
32523241
}
32533242

32543243
callback(error, null);
@@ -3486,8 +3475,14 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
34863475
then(res => ([res, null])).
34873476
catch(error => ([null, error]));
34883477

3478+
const writeErrorsByIndex = {};
3479+
if (error?.writeErrors) {
3480+
for (const writeError of error.writeErrors) {
3481+
writeErrorsByIndex[writeError.err.index] = writeError;
3482+
}
3483+
}
34893484
for (let i = 0; i < validOpIndexes.length; ++i) {
3490-
results[validOpIndexes[i]] = null;
3485+
results[validOpIndexes[i]] = writeErrorsByIndex[i] ?? null;
34913486
}
34923487
if (error) {
34933488
if (validationErrors.length > 0) {
@@ -4386,6 +4381,7 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
43864381
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
43874382
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
43884383
* @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
4384+
* @param {Boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. Set this option to `true` if populating multiple paths or paths with multiple models in transactions.
43894385
* @return {Promise}
43904386
* @api public
43914387
*/
@@ -4403,11 +4399,21 @@ Model.populate = async function populate(docs, paths) {
44034399
}
44044400

44054401
// each path has its own query options and must be executed separately
4406-
const promises = [];
4407-
for (const path of paths) {
4408-
promises.push(_populatePath(this, docs, path));
4402+
if (paths.find(p => p.ordered)) {
4403+
// Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
4404+
// one transaction in parallel.
4405+
// Note that if _any_ path has `ordered`, we make the top-level populate `ordered` as well.
4406+
for (const path of paths) {
4407+
await _populatePath(this, docs, path);
4408+
}
4409+
} else {
4410+
// By default, populate in parallel
4411+
const promises = [];
4412+
for (const path of paths) {
4413+
promises.push(_populatePath(this, docs, path));
4414+
}
4415+
await Promise.all(promises);
44094416
}
4410-
await Promise.all(promises);
44114417

44124418
return docs;
44134419
};
@@ -4527,12 +4533,22 @@ async function _populatePath(model, docs, populateOptions) {
45274533
return;
45284534
}
45294535

4530-
const promises = [];
4531-
for (const arr of params) {
4532-
promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
4536+
if (populateOptions.ordered) {
4537+
// Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
4538+
// one transaction in parallel.
4539+
for (const arr of params) {
4540+
await _execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); });
4541+
}
4542+
} else {
4543+
// By default, populate in parallel
4544+
const promises = [];
4545+
for (const arr of params) {
4546+
promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
4547+
}
4548+
4549+
await Promise.all(promises);
45334550
}
45344551

4535-
await Promise.all(promises);
45364552

45374553
for (const arr of params) {
45384554
const mod = arr[0];

lib/types/double.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Double type constructor
3+
*
4+
* #### Example:
5+
*
6+
* const pi = new mongoose.Types.Double(3.1415);
7+
*
8+
* @constructor Double
9+
*/
10+
11+
'use strict';
12+
13+
module.exports = require('bson').Double;

lib/types/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ exports.Document = // @deprecate
1212
exports.Embedded = require('./arraySubdocument');
1313

1414
exports.DocumentArray = require('./documentArray');
15+
exports.Double = require('./double');
1516
exports.Decimal128 = require('./decimal128');
1617
exports.ObjectId = require('./objectid');
1718

lib/utils.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,8 @@ exports.populate = function populate(path, select, model, match, options, subPop
551551
};
552552
}
553553

554-
if (typeof obj.path !== 'string') {
555-
throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
554+
if (typeof obj.path !== 'string' && !(Array.isArray(obj.path) && obj.path.every(el => typeof el === 'string'))) {
555+
throw new TypeError('utils.populate: invalid path. Expected string or array of strings. Got typeof `' + typeof path + '`');
556556
}
557557

558558
return _populateObj(obj);
@@ -600,7 +600,11 @@ function _populateObj(obj) {
600600
}
601601

602602
const ret = [];
603-
const paths = oneSpaceRE.test(obj.path) ? obj.path.split(manySpaceRE) : [obj.path];
603+
const paths = oneSpaceRE.test(obj.path)
604+
? obj.path.split(manySpaceRE)
605+
: Array.isArray(obj.path)
606+
? obj.path
607+
: [obj.path];
604608
if (obj.options != null) {
605609
obj.options = clone(obj.options);
606610
}

test/bigint.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,22 @@ describe('BigInt', function() {
106106
});
107107

108108
it('is stored as a long in MongoDB', async function() {
109-
await Test.create({ myBigInt: 42n });
109+
await Test.create({ myBigInt: 9223372036854775807n });
110110

111111
const doc = await Test.findOne({ myBigInt: { $type: 'long' } });
112112
assert.ok(doc);
113-
assert.strictEqual(doc.myBigInt, 42n);
113+
assert.strictEqual(doc.myBigInt, 9223372036854775807n);
114114
});
115115

116116
it('becomes a bigint with lean using useBigInt64', async function() {
117-
await Test.create({ myBigInt: 7n });
117+
await Test.create({ myBigInt: 9223372036854775807n });
118118

119119
const doc = await Test.
120-
findOne({ myBigInt: 7n }).
120+
findOne({ myBigInt: 9223372036854775807n }).
121121
setOptions({ useBigInt64: true }).
122122
lean();
123123
assert.ok(doc);
124-
assert.strictEqual(doc.myBigInt, 7n);
124+
assert.strictEqual(doc.myBigInt, 9223372036854775807n);
125125
});
126126

127127
it('can query with comparison operators', async function() {

test/document.populate.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,4 +1075,44 @@ describe('document.populate', function() {
10751075
assert.deepStrictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0]._id, code._id);
10761076
assert.strictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0].code, 'test code');
10771077
});
1078+
1079+
it('supports populate with ordered option (gh-15231)', async function() {
1080+
const docSchema = new Schema({
1081+
refA: { type: Schema.Types.ObjectId, ref: 'Test1' },
1082+
refB: { type: Schema.Types.ObjectId, ref: 'Test2' },
1083+
refC: { type: Schema.Types.ObjectId, ref: 'Test3' }
1084+
});
1085+
1086+
const doc1Schema = new Schema({ name: String });
1087+
const doc2Schema = new Schema({ title: String });
1088+
const doc3Schema = new Schema({ content: String });
1089+
1090+
const Doc = db.model('Test', docSchema);
1091+
const Doc1 = db.model('Test1', doc1Schema);
1092+
const Doc2 = db.model('Test2', doc2Schema);
1093+
const Doc3 = db.model('Test3', doc3Schema);
1094+
1095+
const doc1 = await Doc1.create({ name: 'test 1' });
1096+
const doc2 = await Doc2.create({ title: 'test 2' });
1097+
const doc3 = await Doc3.create({ content: 'test 3' });
1098+
1099+
const docD = await Doc.create({
1100+
refA: doc1._id,
1101+
refB: doc2._id,
1102+
refC: doc3._id
1103+
});
1104+
1105+
await docD.populate({
1106+
path: ['refA', 'refB', 'refC'],
1107+
ordered: true
1108+
});
1109+
1110+
assert.ok(docD.populated('refA'));
1111+
assert.ok(docD.populated('refB'));
1112+
assert.ok(docD.populated('refC'));
1113+
1114+
assert.equal(docD.refA.name, 'test 1');
1115+
assert.equal(docD.refB.title, 'test 2');
1116+
assert.equal(docD.refC.content, 'test 3');
1117+
});
10781118
});

0 commit comments

Comments
 (0)