Skip to content

Commit ee8bf75

Browse files
authored
Merge branch '8.10' into vkarpov15/sync-indexes-tocreate
2 parents 45b20f0 + dcca5ca commit ee8bf75

26 files changed

+635
-62
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ module.exports = {
104104
// 'markdown'
105105
],
106106
parserOptions: {
107-
ecmaVersion: 2020
107+
ecmaVersion: 2022
108108
},
109109
env: {
110110
node: true,

lib/aggregate.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const getConstructorName = require('./helpers/getConstructorName');
1313
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
1414
const stringifyFunctionOperators = require('./helpers/aggregate/stringifyFunctionOperators');
1515
const utils = require('./utils');
16+
const { modelSymbol } = require('./helpers/symbols');
1617
const read = Query.prototype.read;
1718
const readConcern = Query.prototype.readConcern;
1819

@@ -46,13 +47,17 @@ const validRedactStringValues = new Set(['$$DESCEND', '$$PRUNE', '$$KEEP']);
4647
* @see MongoDB https://www.mongodb.com/docs/manual/applications/aggregation/
4748
* @see driver https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#aggregate
4849
* @param {Array} [pipeline] aggregation pipeline as an array of objects
49-
* @param {Model} [model] the model to use with this aggregate.
50+
* @param {Model|Connection} [modelOrConn] the model or connection to use with this aggregate.
5051
* @api public
5152
*/
5253

53-
function Aggregate(pipeline, model) {
54+
function Aggregate(pipeline, modelOrConn) {
5455
this._pipeline = [];
55-
this._model = model;
56+
if (modelOrConn == null || modelOrConn[modelSymbol]) {
57+
this._model = modelOrConn;
58+
} else {
59+
this._connection = modelOrConn;
60+
}
5661
this.options = {};
5762

5863
if (arguments.length === 1 && Array.isArray(pipeline)) {
@@ -1029,12 +1034,24 @@ Aggregate.prototype.pipeline = function() {
10291034
*/
10301035

10311036
Aggregate.prototype.exec = async function exec() {
1032-
if (!this._model) {
1037+
if (!this._model && !this._connection) {
10331038
throw new Error('Aggregate not bound to any Model');
10341039
}
10351040
if (typeof arguments[0] === 'function') {
10361041
throw new MongooseError('Aggregate.prototype.exec() no longer accepts a callback');
10371042
}
1043+
1044+
if (this._connection) {
1045+
if (!this._pipeline.length) {
1046+
throw new MongooseError('Aggregate has empty pipeline');
1047+
}
1048+
1049+
this._optionsForExec();
1050+
1051+
const cursor = await this._connection.client.db().aggregate(this._pipeline, this.options);
1052+
return await cursor.toArray();
1053+
}
1054+
10381055
const model = this._model;
10391056
const collection = this._model.collection;
10401057

lib/connection.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,18 @@ Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
17421742
* @api public
17431743
*/
17441744

1745+
/**
1746+
* Runs a [db-level aggregate()](https://www.mongodb.com/docs/manual/reference/method/db.aggregate/) on this connection's underlying `db`
1747+
*
1748+
* @method aggregate
1749+
* @memberOf Connection
1750+
* @param {Array} pipeline
1751+
* @param {Object} [options]
1752+
* @param {Boolean} [options.cursor=false] If true, make the Aggregate resolve to a Mongoose AggregationCursor rather than an array
1753+
* @return {Aggregate} Aggregation wrapper
1754+
* @api public
1755+
*/
1756+
17451757
/**
17461758
* Removes the database connection with the given name created with with `useDb()`.
17471759
*

lib/cursor/aggregationCursor.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,17 @@ function AggregationCursor(agg) {
4141
this.cursor = null;
4242
this.agg = agg;
4343
this._transforms = [];
44+
const connection = agg._connection;
4445
const model = agg._model;
4546
delete agg.options.cursor.useMongooseAggCursor;
4647
this._mongooseOptions = {};
4748

48-
_init(model, this, agg);
49+
if (connection) {
50+
this.cursor = connection.db.aggregate(agg._pipeline, agg.options || {});
51+
setImmediate(() => this.emit('cursor', this.cursor));
52+
} else {
53+
_init(model, this, agg);
54+
}
4955
}
5056

5157
util.inherits(AggregationCursor, Readable);

lib/document.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -807,8 +807,8 @@ function init(self, obj, doc, opts, prefix) {
807807
reason: e
808808
}));
809809
}
810-
} else if (opts.hydratedPopulatedDocs) {
811-
doc[i] = schemaType.cast(value, self, true);
810+
} else if (schemaType && opts.hydratedPopulatedDocs) {
811+
doc[i] = schemaType.cast(value, self, true, undefined, { hydratedPopulatedDocs: true });
812812

813813
if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) {
814814
self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options);
@@ -4256,24 +4256,25 @@ function applySchemaTypeTransforms(self, json) {
42564256

42574257
for (const path of paths) {
42584258
const schematype = schema.paths[path];
4259-
if (typeof schematype.options.transform === 'function') {
4259+
const topLevelTransformFunction = schematype.options.transform ?? schematype.constructor?.defaultOptions?.transform;
4260+
const embeddedSchemaTypeTransformFunction = schematype.$embeddedSchemaType?.options?.transform
4261+
?? schematype.$embeddedSchemaType?.constructor?.defaultOptions?.transform;
4262+
if (typeof topLevelTransformFunction === 'function') {
42604263
const val = self.$get(path);
42614264
if (val === undefined) {
42624265
continue;
42634266
}
4264-
const transformedValue = schematype.options.transform.call(self, val);
4267+
const transformedValue = topLevelTransformFunction.call(self, val);
42654268
throwErrorIfPromise(path, transformedValue);
42664269
utils.setValue(path, transformedValue, json);
4267-
} else if (schematype.$embeddedSchemaType != null &&
4268-
typeof schematype.$embeddedSchemaType.options.transform === 'function') {
4270+
} else if (typeof embeddedSchemaTypeTransformFunction === 'function') {
42694271
const val = self.$get(path);
42704272
if (val === undefined) {
42714273
continue;
42724274
}
42734275
const vals = [].concat(val);
4274-
const transform = schematype.$embeddedSchemaType.options.transform;
42754276
for (let i = 0; i < vals.length; ++i) {
4276-
const transformedValue = transform.call(self, vals[i]);
4277+
const transformedValue = embeddedSchemaTypeTransformFunction.call(self, vals[i]);
42774278
vals[i] = transformedValue;
42784279
throwErrorIfPromise(path, transformedValue);
42794280
}

lib/drivers/node-mongodb-native/connection.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ NativeConnection.prototype.useDb = function(name, options) {
132132
return newConn;
133133
};
134134

135+
/**
136+
* Runs a [db-level aggregate()](https://www.mongodb.com/docs/manual/reference/method/db.aggregate/) on this connection's underlying `db`
137+
*
138+
* @param {Array} pipeline
139+
* @param {Object} [options]
140+
*/
141+
142+
NativeConnection.prototype.aggregate = function aggregate(pipeline, options) {
143+
return new this.base.Aggregate(null, this).append(pipeline).option(options ?? {});
144+
};
145+
135146
/**
136147
* Removes the database connection with the given name created with `useDb()`.
137148
*

lib/model.js

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,62 @@ Model.prototype.$isMongooseModelPrototype = true;
151151

152152
Model.prototype.db;
153153

154+
/**
155+
* Changes the Connection instance this model uses to make requests to MongoDB.
156+
* This function is most useful for changing the Connection that a Model defined using `mongoose.model()` uses
157+
* after initialization.
158+
*
159+
* #### Example:
160+
*
161+
* await mongoose.connect('mongodb://127.0.0.1:27017/db1');
162+
* const UserModel = mongoose.model('User', mongoose.Schema({ name: String }));
163+
* UserModel.connection === mongoose.connection; // true
164+
*
165+
* const conn2 = await mongoose.createConnection('mongodb://127.0.0.1:27017/db2').asPromise();
166+
* UserModel.useConnection(conn2); // `UserModel` now stores documents in `db2`, not `db1`
167+
*
168+
* UserModel.connection === mongoose.connection; // false
169+
* UserModel.connection === conn2; // true
170+
*
171+
* conn2.model('User') === UserModel; // true
172+
* mongoose.model('User'); // Throws 'MissingSchemaError'
173+
*
174+
* Note: `useConnection()` does **not** apply any [connection-level plugins](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.plugin()) from the new connection.
175+
* If you use `useConnection()` to switch a model's connection, the model will still have the old connection's plugins.
176+
*
177+
* @function useConnection
178+
* @param [Connection] connection The new connection to use
179+
* @return [Model] this
180+
* @api public
181+
*/
182+
183+
Model.useConnection = function useConnection(connection) {
184+
if (!connection) {
185+
throw new Error('Please provide a connection.');
186+
}
187+
if (this.db) {
188+
delete this.db.models[this.modelName];
189+
delete this.prototype.db;
190+
delete this.prototype[modelDbSymbol];
191+
delete this.prototype.collection;
192+
delete this.prototype.$collection;
193+
delete this.prototype[modelCollectionSymbol];
194+
}
195+
196+
this.db = connection;
197+
const collection = connection.collection(this.modelName, connection.options);
198+
this.prototype.collection = collection;
199+
this.prototype.$collection = collection;
200+
this.prototype[modelCollectionSymbol] = collection;
201+
this.prototype.db = connection;
202+
this.prototype[modelDbSymbol] = connection;
203+
this.collection = collection;
204+
this.$__collection = collection;
205+
connection.models[this.modelName] = this;
206+
207+
return this;
208+
};
209+
154210
/**
155211
* The collection instance this model uses.
156212
* A Mongoose collection is a thin wrapper around a [MongoDB Node.js driver collection]([MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html)).
@@ -1246,19 +1302,21 @@ Model.syncIndexes = async function syncIndexes(options) {
12461302
throw new MongooseError('Model.syncIndexes() no longer accepts a callback');
12471303
}
12481304

1249-
const model = this;
1305+
const autoCreate = options?.autoCreate ?? this.schema.options?.autoCreate ?? this.db.config.autoCreate ?? true;
12501306

1251-
try {
1252-
await model.createCollection();
1253-
} catch (err) {
1254-
if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
1255-
throw err;
1307+
if (autoCreate) {
1308+
try {
1309+
await this.createCollection();
1310+
} catch (err) {
1311+
if (err != null && (err.name !== 'MongoServerError' || err.code !== 48)) {
1312+
throw err;
1313+
}
12561314
}
12571315
}
12581316

1259-
const diffIndexesResult = await model.diffIndexes({ indexOptionsToCreate: true });
1260-
const dropped = await model.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop });
1261-
await model.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate });
1317+
const diffIndexesResult = await this.diffIndexes({ indexOptionsToCreate: true });
1318+
const dropped = await this.cleanIndexes({ ...options, toDrop: diffIndexesResult.toDrop });
1319+
await this.createIndexes({ ...options, toCreate: diffIndexesResult.toCreate });
12621320

12631321
return dropped;
12641322
};
@@ -1471,7 +1529,7 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
14711529
* @param {Object} [options]
14721530
* @param {Array<String>} [options.toDrop] if specified, contains a list of index names to drop
14731531
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
1474-
* @return {Promise<String>} list of dropped or hidden index names
1532+
* @return {Promise<Array<String>>} list of dropped or hidden index names
14751533
* @api public
14761534
*/
14771535

@@ -2115,9 +2173,8 @@ Model.estimatedDocumentCount = function estimatedDocumentCount(options) {
21152173
*
21162174
* #### Example:
21172175
*
2118-
* Adventure.countDocuments({ type: 'jungle' }, function (err, count) {
2119-
* console.log('there are %d jungle adventures', count);
2120-
* });
2176+
* const count = await Adventure.countDocuments({ type: 'jungle' });
2177+
* console.log('there are %d jungle adventures', count);
21212178
*
21222179
* If you want to count all documents in a large collection,
21232180
* use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/model.html#Model.estimatedDocumentCount())
@@ -2627,6 +2684,10 @@ Model.create = async function create(doc, options) {
26272684

26282685
delete options.aggregateErrors; // dont pass on the option to "$save"
26292686

2687+
if (options.session && !options.ordered && args.length > 1) {
2688+
throw new MongooseError('Cannot call `create()` with a session and multiple documents unless `ordered: true` is set');
2689+
}
2690+
26302691
if (options.ordered) {
26312692
for (let i = 0; i < args.length; i++) {
26322693
try {
@@ -2713,6 +2774,49 @@ Model.create = async function create(doc, options) {
27132774
return res;
27142775
};
27152776

2777+
/**
2778+
* Shortcut for saving one document to the database.
2779+
* `MyModel.insertOne(obj, options)` is almost equivalent to `new MyModel(obj).save(options)`.
2780+
* The difference is that `insertOne()` checks if `obj` is already a document, and checks for discriminators.
2781+
*
2782+
* This function triggers the following middleware.
2783+
*
2784+
* - `save()`
2785+
*
2786+
* #### Example:
2787+
*
2788+
* // Insert one new `Character` document
2789+
* const character = await Character.insertOne({ name: 'Jean-Luc Picard' });
2790+
* character.name; // 'Jean-Luc Picard'
2791+
*
2792+
* // Create a new character within a transaction.
2793+
* await Character.insertOne({ name: 'Jean-Luc Picard' }, { session });
2794+
*
2795+
* @param {Object|Document} doc Document to insert, as a POJO or Mongoose document
2796+
* @param {Object} [options] Options passed down to `save()`.
2797+
* @return {Promise<Document>} resolves to the saved document
2798+
* @api public
2799+
*/
2800+
2801+
Model.insertOne = async function insertOne(doc, options) {
2802+
_checkContext(this, 'insertOne');
2803+
2804+
const discriminatorKey = this.schema.options.discriminatorKey;
2805+
const Model = this.discriminators && doc[discriminatorKey] != null ?
2806+
this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
2807+
this;
2808+
if (Model == null) {
2809+
throw new MongooseError(
2810+
`Discriminator "${doc[discriminatorKey]}" not found for model "${this.modelName}"`
2811+
);
2812+
}
2813+
if (!(doc instanceof Model)) {
2814+
doc = new Model(doc);
2815+
}
2816+
2817+
return await doc.$save(options);
2818+
};
2819+
27162820
/**
27172821
* _Requires a replica set running MongoDB >= 3.6.0._ Watches the
27182822
* underlying collection for changes using

lib/schema.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,13 +1939,11 @@ Schema.prototype.pre = function(name) {
19391939
* const Model = mongoose.model('Model', schema);
19401940
*
19411941
* const m = new Model(..);
1942-
* m.save(function(err) {
1943-
* console.log('this fires after the `post` hook');
1944-
* });
1942+
* await m.save();
1943+
* console.log('this fires after the `post` hook');
19451944
*
1946-
* m.find(function(err, docs) {
1947-
* console.log('this fires after the post find hook');
1948-
* });
1945+
* await m.find();
1946+
* console.log('this fires after the post find hook');
19491947
*
19501948
* @param {String|RegExp|String[]} methodName The method name or regular expression to match method name
19511949
* @param {Object} [options]
@@ -2382,9 +2380,15 @@ Schema.prototype.virtual = function(name, options) {
23822380
const PopulateModel = this.db.model(modelNames[0]);
23832381
for (let i = 0; i < populatedVal.length; ++i) {
23842382
if (!populatedVal[i].$__) {
2385-
populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
2383+
populatedVal[i] = PopulateModel.hydrate(populatedVal[i], null, { hydratedPopulatedDocs: true });
23862384
}
23872385
}
2386+
const foreignField = options.foreignField;
2387+
this.$populated(
2388+
name,
2389+
populatedVal.map(doc => doc == null ? doc : doc.get(typeof foreignField === 'function' ? foreignField.call(doc, doc) : foreignField)),
2390+
{ populateModelSymbol: PopulateModel }
2391+
);
23882392
}
23892393
}
23902394

lib/schema/array.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,9 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
403403
opts.arrayPathIndex = i;
404404
}
405405
}
406+
if (options.hydratedPopulatedDocs) {
407+
opts.hydratedPopulatedDocs = options.hydratedPopulatedDocs;
408+
}
406409
rawValue[i] = caster.applySetters(rawValue[i], doc, init, void 0, opts);
407410
}
408411
} catch (e) {

0 commit comments

Comments
 (0)