diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js index 15b765af6a..ecbf121f94 100644 --- a/lib/helpers/populate/assignVals.js +++ b/lib/helpers/populate/assignVals.js @@ -137,6 +137,7 @@ module.exports = function assignVals(o) { valueToSet.$__.parent = docs[i]; } + const pathToSet = populateOptions.as || _path; if (o.isVirtual && isDoc) { docs[i].$populated(_path, o.justOne ? originalIds[0] : originalIds, o.allOptions); // If virtual populate and doc is already init-ed, need to walk through @@ -145,7 +146,7 @@ module.exports = function assignVals(o) { valueToSet = valueToSet.map(v => v == null ? void 0 : v); } mpath.set( - _path, + pathToSet, valueToSet, docs[i], // Handle setting paths underneath maps using $* by converting arrays into maps of values @@ -212,7 +213,7 @@ module.exports = function assignVals(o) { // If lean, need to check that each individual virtual respects // `justOne`, because you may have a populated virtual with `justOne` // underneath an array. See gh-6867 - mpath.set(_path, valueToSet, docs[i], lookupLocalFields, setValue, false); + mpath.set(pathToSet, valueToSet, docs[i], lookupLocalFields, setValue, false); if (docs[i].$__) { markArraySubdocsPopulated(docs[i], [o.allOptions.options]); @@ -284,7 +285,7 @@ function valueFilter(val, assignmentOpts, populateOptions, allIds) { maybeRemoveId(subdoc, assignmentOpts); ret.push(subdoc); if (assignmentOpts.originalLimit && - ret.length >= assignmentOpts.originalLimit) { + ret.length >= assignmentOpts.originalLimit) { break; } } diff --git a/lib/model.js b/lib/model.js index 74d990b68c..14ed6b1d37 100644 --- a/lib/model.js +++ b/lib/model.js @@ -631,7 +631,7 @@ Model.prototype.$save = Model.prototype.save; * @instance */ -Model.prototype.$__version = function(where, delta) { +Model.prototype.$__version = function (where, delta) { const key = this.$__schema.options.versionKey; if (where === true) { // this is an insert @@ -740,7 +740,7 @@ Model.prototype.$__where = function _where(where) { Model.prototype.deleteOne = function deleteOne(options) { if (typeof options === 'function' || - typeof arguments[1] === 'function') { + typeof arguments[1] === 'function') { throw new MongooseError('Model.prototype.deleteOne() no longer accepts a callback'); } @@ -911,7 +911,7 @@ Model.exists = function exists(filter, options) { * @api public */ -Model.discriminator = function(name, schema, options) { +Model.discriminator = function (name, schema, options) { let model; if (typeof name === 'function') { model = name; @@ -1042,7 +1042,7 @@ Model.init = function init() { } const conn = this.db; - const _ensureIndexes = async() => { + const _ensureIndexes = async () => { const autoIndex = utils.getOption( 'autoIndex', this.schema.options, @@ -1054,7 +1054,7 @@ Model.init = function init() { } return await this.ensureIndexes({ _automatic: true }); }; - const _createSearchIndexes = async() => { + const _createSearchIndexes = async () => { const autoSearchIndex = utils.getOption( 'autoSearchIndex', this.schema.options, @@ -1067,7 +1067,7 @@ Model.init = function init() { return await this.createSearchIndexes(); }; - const _createCollection = async() => { + const _createCollection = async () => { let autoCreate = utils.getOption( 'autoCreate', this.schema.options, @@ -1099,7 +1099,7 @@ Model.init = function init() { const _catch = this.$init.catch; const _this = this; - this.$init.catch = function() { + this.$init.catch = function () { _this.$caught = true; return _catch.apply(_this.$init, arguments); }; @@ -1620,7 +1620,7 @@ function _ensureIndexes(model, options, callback) { let indexError; options = options || {}; - const done = function(err) { + const done = function (err) { if (err && !model.$caught) { model.emit('error', err); } @@ -1638,24 +1638,24 @@ function _ensureIndexes(model, options, callback) { } if (!indexes.length) { - immediate(function() { + immediate(function () { done(); }); return; } // Indexes are created one-by-one - const indexSingleDone = function(err, fields, options, name) { + const indexSingleDone = function (err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); }; - const indexSingleStart = function(fields, options) { + const indexSingleStart = function (fields, options) { model.emit('index-single-start', fields, options); }; const baseSchema = model.schema._baseSchema; const baseSchemaIndexes = baseSchema ? baseSchema.indexes() : []; - immediate(function() { + immediate(function () { // If buffering is off, do this manually. if (options._automatic && !model.collection.collection) { model.collection.addQueue(create, []); @@ -1668,7 +1668,7 @@ function _ensureIndexes(model, options, callback) { function create() { if (options._automatic) { if (model.schema.options.autoIndex === false || - (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { + (model.schema.options.autoIndex == null && model.db.config.autoIndex === false)) { return done(); } } @@ -2326,7 +2326,7 @@ Model.$where = function $where() { * @api public */ -Model.findOneAndUpdate = function(conditions, update, options) { +Model.findOneAndUpdate = function (conditions, update, options) { _checkContext(this, 'findOneAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findOneAndUpdate() no longer accepts a callback'); @@ -2413,7 +2413,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { * @api public */ -Model.findByIdAndUpdate = function(id, update, options) { +Model.findByIdAndUpdate = function (id, update, options) { _checkContext(this, 'findByIdAndUpdate'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { throw new MongooseError('Model.findByIdAndUpdate() no longer accepts a callback'); @@ -2466,7 +2466,7 @@ Model.findByIdAndUpdate = function(id, update, options) { * @api public */ -Model.findOneAndDelete = function(conditions, options) { +Model.findOneAndDelete = function (conditions, options) { _checkContext(this, 'findOneAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { @@ -2503,7 +2503,7 @@ Model.findOneAndDelete = function(conditions, options) { * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ */ -Model.findByIdAndDelete = function(id, options) { +Model.findByIdAndDelete = function (id, options) { _checkContext(this, 'findByIdAndDelete'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') { @@ -2546,7 +2546,7 @@ Model.findByIdAndDelete = function(id, options) { * @api public */ -Model.findOneAndReplace = function(filter, replacement, options) { +Model.findOneAndReplace = function (filter, replacement, options) { _checkContext(this, 'findOneAndReplace'); if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') { @@ -2597,7 +2597,7 @@ Model.findOneAndReplace = function(filter, replacement, options) { Model.create = async function create(doc, options) { if (typeof options === 'function' || - typeof arguments[2] === 'function') { + typeof arguments[2] === 'function') { throw new MongooseError('Model.create() no longer accepts a callback'); } @@ -2629,12 +2629,12 @@ Model.create = async function create(doc, options) { } if (args.length === 2 && - args[0] != null && - args[1] != null && - args[0].session == null && - last && - getConstructorName(last.session) === 'ClientSession' && - !this.schema.path('session')) { + args[0] != null && + args[1] != null && + args[0].session == null && + last && + getConstructorName(last.session) === 'ClientSession' && + !this.schema.path('session')) { // Probably means the user is running into the common mistake of trying // to use a spread to specify options, see gh-7535 utils.warn('WARNING: to pass a `session` to `Model.create()` in ' + @@ -2664,7 +2664,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } let toSave = doc; if (!(toSave instanceof Model)) { @@ -2689,7 +2689,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } let toSave = doc; @@ -2710,7 +2710,7 @@ Model.create = async function create(doc, options) { this; if (Model == null) { throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` + - `found for model "${this.modelName}"`); + `found for model "${this.modelName}"`); } try { let toSave = doc; @@ -2817,7 +2817,7 @@ Model.insertOne = async function insertOne(doc, options) { * @api public */ -Model.watch = function(pipeline, options) { +Model.watch = function (pipeline, options) { _checkContext(this, 'watch'); options = options || {}; @@ -2873,7 +2873,7 @@ Model.watch = function(pipeline, options) { * @api public */ -Model.startSession = function() { +Model.startSession = function () { _checkContext(this, 'startSession'); return this.db.startSession.apply(this.db, arguments); @@ -2995,7 +2995,7 @@ Model.insertMany = async function insertMany(arr, options) { } // We filter all failed pre-validations by removing nulls - const docAttributes = docs.filter(function(doc) { + const docAttributes = docs.filter(function (doc) { return doc != null; }); for (let i = 0; i < docAttributes.length; ++i) { @@ -3033,7 +3033,7 @@ Model.insertMany = async function insertMany(arr, options) { } return []; } - const docObjects = lean ? docAttributes : docAttributes.map(function(doc) { + const docObjects = lean ? docAttributes : docAttributes.map(function (doc) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } @@ -3054,7 +3054,7 @@ Model.insertMany = async function insertMany(arr, options) { // `writeErrors` is a property reported by the MongoDB driver, // just not if there's only 1 error. if (error.writeErrors == null && - (error.result && error.result.result && error.result.result.writeErrors) != null) { + (error.result && error.result.result && error.result.result.writeErrors) != null) { error.writeErrors = error.result.result.writeErrors; } @@ -3281,7 +3281,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) { _checkContext(this, 'bulkWrite'); if (typeof options === 'function' || - typeof arguments[2] === 'function') { + typeof arguments[2] === 'function') { throw new MongooseError('Model.bulkWrite() no longer accepts a callback'); } options = options || {}; @@ -3830,7 +3830,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op * @api public */ -Model.hydrate = function(obj, projection, options) { +Model.hydrate = function (obj, projection, options) { _checkContext(this, 'hydrate'); if (options?.virtuals && options?.hydratedPopulatedDocs === false) { @@ -4004,9 +4004,9 @@ function _update(model, op, conditions, doc, options) { options = typeof options === 'function' ? options : clone(options); const versionKey = model && - model.schema && - model.schema.options && - model.schema.options.versionKey || null; + model.schema && + model.schema.options && + model.schema.options.versionKey || null; decorateUpdateWithVersionKey(doc, options, versionKey); return mq[op](conditions, doc, options); @@ -4331,6 +4331,9 @@ async function _populatePath(model, docs, populateOptions) { ids = utils.array.unique(ids); const assignmentOpts = {}; + if (populateOptions.as) { + assignmentOpts.as = populateOptions.as; + } assignmentOpts.sort = mod && mod.options && mod.options.options && @@ -4341,9 +4344,9 @@ async function _populatePath(model, docs, populateOptions) { // to fail. So delay running lean transform until _after_ // `_assign()` if (mod.options && - mod.options.options && - mod.options.options.lean && - mod.options.options.lean.transform) { + mod.options.options && + mod.options.options.lean && + mod.options.options.lean.transform) { mod.options.options._leanTransform = mod.options.options.lean.transform; mod.options.options.lean = true; } @@ -4475,8 +4478,8 @@ function _execPopulateQuery(mod, match, select) { // field, that's the client's fault. for (const foreignField of mod.foreignField) { if (foreignField !== '_id' && - query.selectedInclusively() && - !isPathSelectedInclusive(query._fields, foreignField)) { + query.selectedInclusively() && + !isPathSelectedInclusive(query._fields, foreignField)) { query.select(foreignField); } } @@ -4614,7 +4617,7 @@ function _assign(model, vals, mod, assignmentOpts) { assignVals({ originalModel: model, // If virtual, make sure to not mutate original field - rawIds: mod.isVirtual ? allIds : mod.allIds, + rawIds: (mod.isVirtual || assignmentOpts.as) ? allIds : mod.allIds, allIds: allIds, unpopulatedValues: mod.unpopulatedValues, foreignField: mod.foreignField, @@ -4741,7 +4744,7 @@ Model.compile = function compile(name, schema, collectionName, connection, base) model.$__collection = collection; // Create custom query constructor - model.Query = function() { + model.Query = function () { Query.apply(this, arguments); }; Object.setPrototypeOf(model.Query.prototype, Query.prototype); @@ -4891,7 +4894,7 @@ Model.__subclass = function subclass(conn, schema, collection) { Model.collection = Model.prototype.collection; Model.$__collection = Model.collection; // Errors handled internally, so ignore - Model.init().catch(() => {}); + Model.init().catch(() => { }); return Model; }; @@ -4947,7 +4950,7 @@ Model.recompileSchema = function recompileSchema() { * @api public */ -Model.inspect = function() { +Model.inspect = function () { return `Model { ${this.modelName} }`; }; diff --git a/test/model.populate.as.test.js b/test/model.populate.as.test.js new file mode 100644 index 0000000000..b799e982cb --- /dev/null +++ b/test/model.populate.as.test.js @@ -0,0 +1,128 @@ + +'use strict'; + +const start = require('./common'); +const assert = require('assert'); +const mongoose = start.mongoose; +const Schema = mongoose.Schema; +const ObjectId = Schema.ObjectId; + +describe('model: populate: as option', function () { + let db; + let User; + let BlogPost; + let user1; + let post; + + before(function () { + db = start(); + }); + + after(async function () { + await db.close(); + }); + + beforeEach(async () => { + await db.deleteModel(/.*/); + + const userSchema = new Schema({ + name: String + }); + User = db.model('User', userSchema); + + const blogPostSchema = new Schema({ + title: String, + authorId: { type: ObjectId, ref: 'User' }, + fans: [{ type: ObjectId, ref: 'User' }], + comments: [{ + content: String, + authorId: { type: ObjectId, ref: 'User' } + }] + }); + BlogPost = db.model('BlogPost', blogPostSchema); + + user1 = await User.create({ name: 'Val' }); + post = await BlogPost.create({ + title: 'Test', + authorId: user1._id, + fans: [user1._id], + comments: [{ content: 'Nice', authorId: user1._id }] + }); + }); + + afterEach(async () => { + await util.clearTestData(db); + }); + + it('should populate to a different path using "as"', async function () { + const doc = await BlogPost.findById(post._id).populate({ + path: 'authorId', + as: 'author' + }); + + assert.ok(doc.authorId instanceof mongoose.Types.ObjectId); + assert.equal(doc.authorId.toString(), user1._id.toString()); + + // Access via .get() or ._doc because 'author' is not in schema + assert.ok(doc.get('author')); + assert.equal(doc.get('author').name, 'Val'); + }); + + it('should populate to a different path using "as" with lean', async function () { + const doc = await BlogPost.findById(post._id).populate({ + path: 'authorId', + as: 'author' + }).lean(); + + assert.ok(doc.authorId instanceof mongoose.Types.ObjectId); + assert.equal(doc.authorId.toString(), user1._id.toString()); + + assert.ok(doc.author); + assert.equal(doc.author.name, 'Val'); + }); + + it('should populate array to a different path using "as"', async function () { + const doc = await BlogPost.findById(post._id).populate({ + path: 'fans', + as: 'fansPopulated' + }); + + assert.equal(doc.fans.length, 1); + assert.ok(doc.fans[0] instanceof mongoose.Types.ObjectId); + + const fansPopulated = doc.get('fansPopulated'); + assert.ok(Array.isArray(fansPopulated)); + assert.equal(fansPopulated.length, 1); + assert.equal(fansPopulated[0].name, 'Val'); + }); + + it('should populate nested path to a different nested path using "as"', async function () { + const doc = await BlogPost.findById(post._id).populate({ + path: 'comments.authorId', + as: 'comments.author' + }); + + assert.equal(doc.comments.length, 1); + assert.ok(doc.comments[0].authorId instanceof mongoose.Types.ObjectId); + + // Accessing nested populated field might be tricky if not in schema + // doc.comments[0] is a Subdocument. + assert.ok(doc.comments[0].get('author')); + assert.equal(doc.comments[0].get('author').name, 'Val'); + }); + + it('should populate nested path to a different nested path using "as" with lean', async function () { + const doc = await BlogPost.findById(post._id).populate({ + path: 'comments.authorId', + as: 'comments.author' + }).lean(); + + assert.equal(doc.comments.length, 1); + assert.ok(doc.comments[0].authorId instanceof mongoose.Types.ObjectId); + + assert.ok(doc.comments[0].author); + assert.equal(doc.comments[0].author.name, 'Val'); + }); +}); + +const util = require('./util');