So there's a couple of ways to do this as mentioned.
Using $lookup
You basically want to get the "related" data from the other collection and "merge" that with the existing array items. You cannot actually just "target" the existing array since $lookup cannot do that, but it can write another array and then you can "merge" them together:
let result1 = await Order.aggregate([ { "$lookup": { "from": Meal.collection.name, "foreignField": "_id", "localField": "meals.meal", "as": "mealitems" }}, { "$project": { "meals": { "$map": { "input": "$meals", "in": { "meal": { "$arrayElemAt": [ "$mealitems", { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] } ] }, "quantity": "$$this.quantity", "totalPrice": { "$multiply": [ { "$arrayElemAt": [ "$mealitems.price", { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] } ]}, "$$this.quantity" ] } } } } }}, { "$addFields": { "totalOrder": { "$sum": "$meals.totalPrice" } }} ]);
That basically produces another array "mealitems" as the result of $lookup and then uses $map in order to process through the original document array and transpose the returned content array items back into the structure for each item.
You do that in combination with $arrayElemAt and $indexOfArray to find the matched items to transpose here.
There is also some "math" for the other computed elements using $multiply, and even an additional $addFields stage using $sum to "add those up" to give an overall "order total" for the document.
You "could" just do all that math in the $project stage ( which is used because we don't want the "mealitems" content. But that's a little more involved and you probably want to use $let for the array matching so you don't repeat your code so much.
You can even use the "sub-pipeline" form of $lookup if you really want to. Instead of using $map as the operations to alter the returned documents are done "inside" the returned array before the results are returned, by transposing the initial document array into the result documents via it's let argument:
// Aggregate with $lookup - sub-pipeline let result2 = await Order.aggregate([ { "$lookup": { "from": Meal.collection.name, "let": { "meals": "$meals" }, "pipeline": [ { "$match": { "$expr": { "$in": [ "$_id", "$$meals.meal" ] } }}, { "$replaceRoot": { "newRoot": { "meal": "$$ROOT", "quantity": { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ] }, "totalPrice": { "$multiply": [ { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ]}, "$price" ] } } }} ], "as": "meals" }}, { "$addFields": { "totalOrder": { "$sum": "$meals.totalPrice" } }} ]);
In either form, that's basically an allegory for what populate() is doing under the hood by "merging" the content, but of course that uses separate database requests where the $lookup aggregation is just one request.
Using populate()
Alternately you can just manipulate the resulting structure in JavaScript. It's already there, and all you really need is the lean() in order to be able to alter the resulting objects:
// Populate and manipulate let result3 = await Order.find().populate('meals.meal').lean(); result3 = result3.map(r => ({ ...r, meals: r.meals.map( m => ({ ...m, totalPrice: m.meal.price * m.quantity }) ), totalOrder: r.meals.reduce((o, m) => o + (m.meal.price * m.quantity), 0 ) }) );
It looks pretty simple and is basically the same thing, with the exceptions that the "merging" was already done for you and that of course this is two requests to the server in order to return all the data.
As a reproducible full listing:
const { Schema } = mongoose = require('mongoose'); // Connection const uri = 'mongodb://localhost:27017/menu'; const opts = { useNewUrlParser: true }; // Sensible defaults mongoose.Promise = global.Promise; mongoose.set('useFindAndModify', false); mongoose.set('useCreateIndex', true); mongoose.set('debug', true); // Schema defs const mealSchema = new Schema({ name: String, price: Number }); const orderSchema = new Schema({ meals: [ { meal: { type: Schema.Types.ObjectId, ref: 'Meal' }, quantity: Number } ] }); const Meal = mongoose.model('Meal', mealSchema); const Order = mongoose.model('Order', orderSchema); // log helper const log = data => console.log(JSON.stringify(data, undefined, 2)); // main (async function() { try { const conn = await mongoose.connect(uri, opts); // clean models await Promise.all( Object.entries(conn.models).map(([k,m]) => m.deleteMany()) ); // Set up data let [Chicken, Beef] = await Meal.insertMany( [ { name: "Chicken Nuggets", price: 3 }, { name: "Beef Burger", price: 6 } ] ); let order = await Order.create({ meals: [ { meal: Chicken, quantity: 12 }, { meal: Beef, quantity: 4 } ] }); // Aggregate with $lookup - traditional let result1 = await Order.aggregate([ { "$lookup": { "from": Meal.collection.name, "foreignField": "_id", "localField": "meals.meal", "as": "mealitems" }}, { "$project": { "meals": { "$map": { "input": "$meals", "in": { "meal": { "$arrayElemAt": [ "$mealitems", { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] } ] }, "quantity": "$$this.quantity", "totalPrice": { "$multiply": [ { "$arrayElemAt": [ "$mealitems.price", { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] } ]}, "$$this.quantity" ] } } } } }}, { "$addFields": { "totalOrder": { "$sum": "$meals.totalPrice" } }} ]); log(result1); // Aggregate with $lookup - sub-pipeline let result2 = await Order.aggregate([ { "$lookup": { "from": Meal.collection.name, "let": { "meals": "$meals" }, "pipeline": [ { "$match": { "$expr": { "$in": [ "$_id", "$$meals.meal" ] } }}, { "$replaceRoot": { "newRoot": { "meal": "$$ROOT", "quantity": { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ] }, "totalPrice": { "$multiply": [ { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ]}, "$price" ] } } }} ], "as": "meals" }}, { "$addFields": { "totalOrder": { "$sum": "$meals.totalPrice" } }} ]); log(result2); // Populate and manipulate let result3 = await Order.find().populate('meals.meal').lean(); result3 = result3.map(r => ({ ...r, meals: r.meals.map( m => ({ ...m, totalPrice: m.meal.price * m.quantity }) ), totalOrder: r.meals.reduce((o, m) => o + (m.meal.price * m.quantity), 0 ) }) ); log(result3); } catch(e) { console.error(e); } finally { mongoose.disconnect(); } })()
Which returns results like:
Mongoose: meals.deleteMany({}, {}) Mongoose: orders.deleteMany({}, {}) Mongoose: meals.insertMany([ { _id: 5bea4c8f6edcd22d385a13bf, name: 'Chicken Nuggets', price: 3, __v: 0 }, { _id: 5bea4c8f6edcd22d385a13c0, name: 'Beef Burger', price: 6, __v: 0 } ], {}) Mongoose: orders.insertOne({ _id: ObjectId("5bea4c8f6edcd22d385a13c1"), meals: [ { _id: ObjectId("5bea4c8f6edcd22d385a13c3"), meal: ObjectId("5bea4c8f6edcd22d385a13bf"), quantity: 12 }, { _id: ObjectId("5bea4c8f6edcd22d385a13c2"), meal: ObjectId("5bea4c8f6edcd22d385a13c0"), quantity: 4 } ], __v: 0 }) Mongoose: orders.aggregate([ { '$lookup': { from: 'meals', foreignField: '_id', localField: 'meals.meal', as: 'mealitems' } }, { '$project': { meals: { '$map': { input: '$meals', in: { meal: { '$arrayElemAt': [ '$mealitems', { '$indexOfArray': [ '$mealitems._id', '$$this.meal' ] } ] }, quantity: '$$this.quantity', totalPrice: { '$multiply': [ { '$arrayElemAt': [ '$mealitems.price', { '$indexOfArray': [Array] } ] }, '$$this.quantity' ] } } } } } }, { '$addFields': { totalOrder: { '$sum': '$meals.totalPrice' } } } ], {}) [ { "_id": "5bea4c8f6edcd22d385a13c1", "meals": [ { "meal": { "_id": "5bea4c8f6edcd22d385a13bf", "name": "Chicken Nuggets", "price": 3, "__v": 0 }, "quantity": 12, "totalPrice": 36 }, { "meal": { "_id": "5bea4c8f6edcd22d385a13c0", "name": "Beef Burger", "price": 6, "__v": 0 }, "quantity": 4, "totalPrice": 24 } ], "totalOrder": 60 } ] Mongoose: orders.aggregate([ { '$lookup': { from: 'meals', let: { meals: '$meals' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$meals.meal' ] } } }, { '$replaceRoot': { newRoot: { meal: '$$ROOT', quantity: { '$arrayElemAt': [ '$$meals.quantity', { '$indexOfArray': [ '$$meals.meal', '$_id' ] } ] }, totalPrice: { '$multiply': [ { '$arrayElemAt': [ '$$meals.quantity', [Object] ] }, '$price' ] } } } } ], as: 'meals' } }, { '$addFields': { totalOrder: { '$sum': '$meals.totalPrice' } } } ], {}) [ { "_id": "5bea4c8f6edcd22d385a13c1", "meals": [ { "meal": { "_id": "5bea4c8f6edcd22d385a13bf", "name": "Chicken Nuggets", "price": 3, "__v": 0 }, "quantity": 12, "totalPrice": 36 }, { "meal": { "_id": "5bea4c8f6edcd22d385a13c0", "name": "Beef Burger", "price": 6, "__v": 0 }, "quantity": 4, "totalPrice": 24 } ], "__v": 0, "totalOrder": 60 } ] Mongoose: orders.find({}, { projection: {} }) Mongoose: meals.find({ _id: { '$in': [ ObjectId("5bea4c8f6edcd22d385a13bf"), ObjectId("5bea4c8f6edcd22d385a13c0") ] } }, { projection: {} }) [ { "_id": "5bea4c8f6edcd22d385a13c1", "meals": [ { "_id": "5bea4c8f6edcd22d385a13c3", "meal": { "_id": "5bea4c8f6edcd22d385a13bf", "name": "Chicken Nuggets", "price": 3, "__v": 0 }, "quantity": 12, "totalPrice": 36 }, { "_id": "5bea4c8f6edcd22d385a13c2", "meal": { "_id": "5bea4c8f6edcd22d385a13c0", "name": "Beef Burger", "price": 6, "__v": 0 }, "quantity": 4, "totalPrice": 24 } ], "__v": 0, "totalOrder": 60 } ]
populate()andaggregate(). There is instead$lookupto use in an aggregation pipeline instead of callingpopulate()altogether. No code here, and without it we can only point you to generic examples.populate()withlean()and manipulate the result object in JavaScript code. A simpleArray.reduce()over the array content should do the job.