Populate + Aggregate on Array - node.js

I am using mongoose (5.x.x) with the populate function to populate an array (meals) with refs.
Within that array I need to multiply a price (result of the populate) and a quantity (part of the base Schema).
My populate result as below:
{
"_id": "5bea354235711482876f8fa8",
"meals": [
{
"meal": {
"_id": "5be93c7074488c77b10fba00",
"name": "Chicken Nuggets",
"price": 3
},
"quantity": 12
},
{
"meal": {
"_id": "5be93c9274488c77b10fba01",
"name": "Beef Burger",
"price": 6
},
"quantity": 4
}
],
"__v": 0
}
The goal would be to add a "total price" within this result set but I can't find any elegant way to do so.
I'd like to avoid manipulating data outside of the query.
Thanks for your help,

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
}
]

Related

How to change this mongo query to return "00:00" if $match condition does not satisfy in aggregate?

Consider I have a timesheets collection like this:
[
{
_id: 1,
createdBy: "John",
duration: "00:30"
},
{
_id: 2,
createdBy: "John",
duration: "01:30"
},
{
_id: 3,
createdBy: "Mark",
duration: "00:30"
},
]
My input is an array of usernames:
["John", "Mark", "Rikio"]
I want to use mongodb aggregate to calculate the total duration of timesheets for each user in the usernames array and If there are no timesheets found, it should return duration: "00:00". For example, it should return:
[
{createdBy: "John", totalDuration: "02:00"},
{createdBy: "Mark", totalDuration: "00:30"},
{createdBy: "Rikio", totalDuration: "00:00"}
]
However, when I use $match query, if there are no timesheets it will not return anything so I don't know which user does not have the timesheets and return "00:00" for them.
I totally agree with #turivishal , but still can make it through mongo query with an ugly one.
db.collection.aggregate([
{
$match: {}
},
{
$set: {
minutes: {
$let: {
vars: {
time: {
$split: [
"$duration",
":"
]
}
},
in: {
"$add": [
{
"$multiply": [
{
$toInt: {
$first: "$$time"
}
},
60
]
},
{
$toInt: {
$last: "$$time"
}
}
]
}
}
}
}
},
{
$group: {
"_id": "$createdBy",
"totalMinutes": {
"$sum": "$minutes"
}
}
},
{
$group: {
"_id": null,
"docs": {
"$push": "$$ROOT"
}
}
},
{
$set: {
"docs": {
$map: {
"input": [
"John",
"Mark",
"Rikio"
],
"as": "name",
"in": {
$let: {
vars: {
findName: {
"$first": {
"$filter": {
"input": "$docs",
"as": "d",
"cond": {
"$eq": [
"$$d._id",
"$$name"
]
}
}
}
}
},
in: {
"$cond": {
"if": "$$findName",
"then": "$$findName",
"else": {
_id: "$$name",
totalMinutes: 0
}
}
}
}
}
}
}
}
},
{
$unwind: "$docs"
},
{
$replaceRoot: {
"newRoot": "$docs"
}
},
{
$set: {
"hours": {
$floor: {
"$divide": [
"$totalMinutes",
60
]
}
},
"minutes": {
"$mod": [
"$totalMinutes",
60
]
}
}
},
{
$set: {
"hours": {
"$cond": {
"if": {
"$lt": [
"$hours",
10
]
},
"then": {
"$concat": [
"0",
{
"$toString": "$hours"
}
]
},
"else": {
"$toString": "$hours"
}
}
},
"minutes": {
"$cond": {
"if": {
"$lt": [
"$minutes",
10
]
},
"then": {
"$concat": [
"0",
{
"$toString": "$minutes"
}
]
},
"else": {
"$toString": "$minutes"
}
}
}
}
},
{
$project: {
duration: {
"$concat": [
"$hours",
":",
"$minutes"
]
}
}
}
])
mongoplayground

How to update an array in mongodb by using another array as both identifier and value?

Imagine the is a document like this:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'name1',
},
{
_id: 'id2',
name: 'name2',
},
{
_id: 'id3',
name: 'name3'
}
]
}
I have an array like this:
const newData = [
{_id: 'id1', name: 'newName1'},
{_id: 'id2', 'name': 'newName2', family:'newFamily2'}
]
what I want is to update the array in the document using the corresponding _id and using it to add/update each element.
so my end result would be like:
{
_id: ObjectID('someIdblahbla')
users: [
{
_id: 'id1',
name: 'newName1',
},
{
_id: 'id2',
name: 'newName2',
family:'newFamily2'
},
{
_id: 'id3',
name: 'name3'
}
]
}
my guess was using The filtered positional operator but I am not sure if it's the correct way to go and how to do it.
thank you for your kind tips beforehand.
There is no straight way to add/update in array, you can use update with aggregation pipeline starting from MongoDB 4.2,
First of all, you need to convert _id from string to objectId type, if you are using mongoose npm you can use mongoose.Types.ObjectId method or if you are using mongodb npm you can use ObjectId method,
let newData = [
{ _id: 'id1', name: 'newName1' },
{ _id: 'id2', 'name': 'newName2', family:'newFamily2' }
];
let newIds = [];
newData = newData.map(n => {
n._id = ObjectId(n._id); // or mongoose.Types.ObjectId(n._id)
newIds.push(n._id); // for checking conditions
return n;
});
You can put query condition, and do below operations,
$map to iterate loop of users array, check condition if user._id is in input newIds then do update operation otherwise do insert operation
update operation:
$filter to iterate loop of input newData and filter already present object from input so we can update it
$arrayElemAt to get first object from above filtered array
$mergeObjects to merge current object with above input object
insert operation:
$filter to iterate loop of newData array and return not present object means new items in array of objects
$concatArrays to concat above new and updated result array
db.collection.updateOne(
{ _id: ObjectId("someIdblahbla") },
[{
$set: {
users: {
$concatArrays: [
{
$map: {
input: "$users",
as: "u",
in: {
$cond: [
{ $in: ["$$u._id", newIds] },
{
$mergeObjects: [
"$$u",
{
$arrayElemAt: [
{
$filter: {
input: newData,
cond: { $eq: ["$$this._id", "$$u._id"] }
}
},
0
]
}
]
},
"$$u"
]
}
}
},
{
$filter: {
input: newData,
cond: { $not: { $in: ["$$this._id", "$users._id"] } }
}
}
]
}
}
}]
)
Playground
Query1 (update(merge objects) existing members, doesn't add new members)
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"}] with you array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"users": {
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
}
],
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
}
}
}
])
Query2 (update(merge) if found, else push in the end)
Its like the above but finds the not-existing members,and push them in the end.Its a bit more slower and complicated
Test code here
Replace
[{"_id": "id1","name": "newName1"},{"_id": "id2","name": "newName2","family": "newFamily2"},{"_id": "id4","name": "newName4"}]
with your array or the driver variable that hold the array
db.collection.update({
"_id": {
"$eq": "1"
}
},
[
{
"$addFields": {
"yourarray": [
{
"_id": "id1",
"name": "newName1"
},
{
"_id": "id2",
"name": "newName2",
"family": "newFamily2"
},
{
"_id": "id4",
"name": "newName4"
}
]
}
},
{
"$addFields": {
"new-ids": {
"$setDifference": [
{
"$map": {
"input": "$yourarray",
"as": "u",
"in": "$$u._id"
}
},
{
"$map": {
"input": "$users",
"as": "u",
"in": "$$u._id"
}
}
]
}
}
},
{
"$addFields": {
"users": {
"$concatArrays": [
{
"$map": {
"input": "$users",
"as": "user",
"in": {
"$reduce": {
"input": "$yourarray",
"initialValue": "$$user",
"in": {
"$let": {
"vars": {
"old_user": "$$value",
"new_user": "$$this"
},
"in": {
"$cond": [
{
"$eq": [
"$$old_user._id",
"$$new_user._id"
]
},
{
"$mergeObjects": [
"$$old_user",
"$$new_user"
]
},
"$$old_user"
]
}
}
}
}
}
}
},
{
"$filter": {
"input": "$yourarray",
"as": "u",
"cond": {
"$in": [
"$$u._id",
"$new-ids"
]
}
}
}
]
}
}
},
{
"$unset": [
"yourarray",
"new-ids"
]
}
])

Mongoose lookup fetching data from multi collection

When i combine 2 table to fetch data from mongoDB collection struck with my expected out. please any one help me to fix the same pleas.
Collection:
recipecatagories
{
"_id":{"$oid":"5dada3c5761bb32a1201d4da"},
"CategoryName":"Biryani"
}
{
"_id":{"$oid":"5dada3c5761bb32a1201d4db"},
"CategoryName":"Mutton Biryani"
}
{
"_id":{"$oid":"5dada3c5761bb32a1201d4d4"},
"CategoryName":"Chicken Biryani"
}
{
"_id":{"$oid":"5daea43a517cf601a7e80a3b"},
"CategoryName":"Kathirikai gothsu"
}
recipes:
{
"_id":{"$oid":"5daffda85d9b4fd19ae4da30"},
"recipeTitle":"Mutton dum biryani",
"Recipetags":["Indian","NonVeg","Lunch"],
"cookTime":"30 Mins",
"recipeCategoryId":[{"$oid":"5dada3c5761bb32a1201d4da"},{"$oid":"5dada3c5761bb32a1201d4db"},{"$oid":"5dada3c5761bb32a1201d4dc"}],
"recipeCuisienId":"Indian",
"recepeType":false,
"availaleStreaming":"TEXT",
"postedOn":{"$date":{"$numberLong":"0"}},
"postedBy":"shiva#yopmail.com"
}
{
"_id":{"$oid":"5daffda85d9b4fd19ae4da30"},
"recipeTitle":"Mutton Chicken biryani",
"Recipetags":["Indian","NonVeg","Lunch"],
"cookTime":"30 Mins",
"recipeCategoryId":[{"$oid":"5dada3c5761bb32a1201d4da"},{"$oid":"5dada3c5761bb32a1201d4d4"},{"$oid":"5dada3c5761bb32a1201d4dc"}],
"recipeCuisienId":"Indian",
"recepeType":false,
"availaleStreaming":"TEXT",
"postedOn":{"$date":{"$numberLong":"0"}},
"postedBy":"shiva#yopmail.com"
}
users:
{
"_id":{"$oid":"5da428b85e3cbd0f153c7f3b"},
"emailId":"shiva#yopmail.com",
"fullName":"siva prakash",
"accessToken":"xxxxxxxxxxxxx",
"__v":{"$numberInt":"0"}
}
Current mongoose code in node js
RecipeCatagory.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id) } },
{
"$lookup": {
"from": "recipes",
"localField": "_id",
"foreignField": "recipeCategoryId",
"as": "recipes"
}
},
{ "$unwind": "$recipes" },
{ "$unwind": "$recipes.recipeCategoryId" },
{
"$match": {
"recipes.recipeCategoryId": mongoose.Types.ObjectId(id)
}
},
{
"$lookup": {
"from": "users",
"localField": "emailId",
"foreignField": "recipes.postedBy",
"as": "users"
}
},
])
.exec(function (err, recipes) {
if (err) {
response
.status(400)
.json({
"status": "Failed",
"message": "Error",
"data": err | err.message
});
return
} else {
response
.status(200)
.json({
"status": "Ok",
"message": "Success",
"data": recipes
});
return
}
})
Current Output using above Query
{
"status": "Ok",
"message": "Success",
"data": [
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "Biryani",
"recipes": {
"_id": "5daffda85d9b4fd19ae4da30",
"recipeTitle": "Mutton dum biryani",
"Recipetags": [
"Indian",
"NonVeg",
"Lunch"
],
"cookTime": "30 Mins",
"recipeCategoryId": "5dada3c5761bb32a1201d4da",
"recipeCuisienId": "Indian",
"recepeType": false,
"availaleStreaming": "TEXT",
"postedOn": "1970-01-01T00:00:00.000Z",
"postedBy": "shiva#yopmail.com"
},
"users": [
{
"_id": "5da428b85e3cbd0f153c7f3b",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "42eb19a0-ee57-11e9-86f7-a7b758fb7271",
"__v": 0
}
]
},
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "Biryani",
"recipes": {
"_id": "5daffda85d9b4fd19ae4da31",
"recipeTitle": "Kumbakonam kathirikai gothsu",
"Recipetags": [
"Indian",
"Veg"
],
"cookTime": "30 Mins",
"recipeCategoryId": "5dada3c5761bb32a1201d4da",
"recipeCuisienId": "Indian",
"recepeType": true,
"availaleStreaming": "TEXT",
"postedOn": "1970-01-01T00:00:00.000Z",
"postedBy": "shiva#yopmail.com"
},
"users": [
{
"_id": "5da428b85e3cbd0f153c7f3b",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "xxxxxxxxxxxxx",
"__v": 0
}
]
}
]
}
**Expected Out:**
{
"status": "Ok",
"message": "Success",
"data": [
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "chiken Biryani",
"recipes": {
"_id": "5daffda85d9b4fd19ae4da30",
"recipeTitle": "Mutton dum biryani",
"Recipetags": [
"Indian",
"NonVeg",
"Lunch"
],
"cookTime": "30 Mins",
"recipeCategoryId": "5dada3c5761bb32a1201d4da",
"recipeCuisienId": "Indian",
"recepeType": false,
"availaleStreaming": "TEXT",
"postedOn": "1970-01-01T00:00:00.000Z",
"postedBy": "shiva#yopmail.com"
},
"users": [
{
"_id": "5da428b85e3cbd0f153c7f3b",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "42eb19a0-ee57-11e9-86f7-a7b758fb7271",
"__v": 0
}
]
},
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "Biryani",
"recipes": [
{
"_id": "5daffda85d9b4fd19ae4da31",
"recipeTitle": "Mutton dum biryani",
"Recipetags": [
"Indian",
"Veg"
],
"cookTime": "30 Mins",
"recipeCategoryId": "5dada3c5761bb32a1201d4da",
"recipeCuisienId": "Indian",
"recepeType": true,
"availaleStreaming": "TEXT",
"postedOn": "1970-01-01T00:00:00.000Z",
"postedBy": "shiva#yopmail.com"
},
{
"_id": "5daffda85d9b4fd19ae4da31",
"recipeTitle": "Chicken biryani",
"Recipetags": [
"Indian",
"Veg"
],
"cookTime": "30 Mins",
"recipeCategoryId": "5dada3c5761bb32a1201d4da",
"recipeCuisienId": "Indian",
"recepeType": true,
"availaleStreaming": "TEXT",
"postedOn": "1970-01-01T00:00:00.000Z",
"postedBy": "shiva#yopmail.com"
}
],
"users": [
{
"_id": "5da428b85e3cbd0f153c7f3b",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "xxxxxxxxxxxx",
"__v": 0
}
]
}
]
}
i am struck to get expected out put... i want recipes as array which has recipecategory has in recipe collection...
You are basically doing this the wrong way around and should instead be querying from the Recipe model. You do already have the "category id values" which are contained within an array of that document.
Basically you should have something like this:
const wantedCategories = [
ObjectId("5dada3c5761bb32a1201d4da"),
ObjectId("5dada3c5761bb32a1201d4db")
];
let data = await Recipe.aggregate([
// Match wanted category(ies)
{ "$match": {
"recipeCategoryId": { "$in": wantedCategories }
}},
// Filter the content of the array
{ "$addFields": {
"recipeCategoryId": {
"$filter": {
"input": "$recipeCategoryId",
"cond": {
"$in": [ "$$this", wantedCategories ]
}
}
}
}},
// Lookup the related matching category(ies)
{ "$lookup": {
"from": RecipeCategory.collection.name,
"let": { "recipeCategoryIds": "$recipeCategoryId" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$recipeCategoryIds" ] }
}}
],
"as": "recipeCategoryId"
}},
// Lookup the related user to postedBy
{ "$lookup": {
"from": User.collection.name,
"let": { "postedBy": "$postedBy" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$emailId", "$$postedBy" ] } } }
],
"as": "postedBy"
}},
// postedBy is "singular"
{ "$unwind": "$postedBy" }
]);
Which would return a result like this:
{
"data": [
{
"_id": "5dbce992010163139853168c",
"Recipetags": [
"Indian",
"NonVeg",
"Lunch"
],
"recipeCategoryId": [
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "Biryani",
"__v": 0
},
{
"_id": "5dada3c5761bb32a1201d4db",
"CategoryName": "Mutton Biryani",
"__v": 0
}
],
"recipeTitle": "Mutton dum biryani",
"cookTime": "30 Mins",
"recepeType": false,
"postedBy": {
"_id": "5dbce992010163139853168e",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "xxxxxxxxxxxxx",
"__v": 0
},
"__v": 0
},
{
"_id": "5dbce992010163139853168d",
"Recipetags": [
"Indian",
"NonVeg",
"Lunch"
],
"recipeCategoryId": [
{
"_id": "5dada3c5761bb32a1201d4da",
"CategoryName": "Biryani",
"__v": 0
}
],
"recipeTitle": "Mutton Chicken biryani",
"cookTime": "30 Mins",
"recepeType": false,
"postedBy": {
"_id": "5dbce992010163139853168e",
"emailId": "shiva#yopmail.com",
"fullName": "siva prakash",
"accessToken": "xxxxxxxxxxxxx",
"__v": 0
},
"__v": 0
}
]
}
Note: I do actually correct the english spelling of a model with RecipeCategory instead of RecipeCatagory as shown in the question. Apply to your own implementation as you wish.
You might note the usage of $in with a "list of ids" in both the query form and the aggregation operator form at different stages. Personally I would code this in this way even if there was only a single value supplied at the present time, since it means there would be little to change other than the input variable to the method in the event I wanted multiple values, such as "multiple categories" within a faceted search option for example.
So this demonstrates the "list" argument approach, though it still applies to singular values as in the question.
The whole process follows what the comments say on each pipeline stage, being that you first match wanted "documents" from the recipes collection by the selected "category" value(s). Then we just want to remove any non-matching data for the category within the array of those documents. This could actually be viewed as "optional" if you wanted to just show ALL categories associated with that recipe whether they matched the criteria or not. Where this is the case, all you need do is remove the stage containing the $filter statement, and the code will happily work in that way.
Then of course there are the $lookup stages, being one for each related collection. The example here actually shows the expressive form of the $lookup pipeline stage. This again is really only for demonstration as the standard localField and foreignField form is perfectly fine for the purposes of what you want to do here, and the further syntax is not needed. MongoDB will basically transform that older syntax into the newer expressive form as shown internally anyway.
You might note the usage of Model.collection.name in the from argument though. This is actually a handy thing to do when coding with mongoose. MongoDB itself expects the actual collection name as the argument here. Since mongoose will typically pluralize the model name provided for the actual collection referenced, or otherwise takes an explicit argument to the model definition, then using the .collection.name accessor from the model ensures you have the correct actual collection name, even if this changes at some time within the model definition.
The only other simple step here is the $unwind at the end, and only because the output of $lookup is always an array, and here the replacement of the postedBy property with matched related content is always expected to be only one item. So for simple readability of results, we can just make this a single value instead of having an array here.
For a bit more context into how that all comes together, here is the code for the statement and the creation of the data all in a self contained listing, from which of course the "output" posted above was actually obtained:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/menu';
const options = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const recipeCategorySchema = new Schema({
CategoryName: String
});
const recipeSchema = new Schema({
recipeTitle: String,
Recipetags: [String],
cookTime: String,
recipeCategoryId: [{ type: Schema.Types.ObjectId, ref: 'RecipeCategory' }],
recipeCuisineId: String,
recepeType: Boolean,
availableStreaming: String,
postedBy: String
});
const userSchema = new Schema({
emailId: String,
fullName: String,
accessToken: String
});
const RecipeCategory = mongoose.model('RecipeCategory', recipeCategorySchema);
const Recipe = mongoose.model('Recipe', recipeSchema);
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data for demonstration
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
// Insert some data
await RecipeCategory.insertMany([
{
"_id": ObjectId( "5dada3c5761bb32a1201d4da"),
"CategoryName":"Biryani"
},
{
"_id": ObjectId("5dada3c5761bb32a1201d4db"),
"CategoryName":"Mutton Biryani"
},
{
"_id": ObjectId("5dada3c5761bb32a1201d4d4"),
"CategoryName":"Chicken Biryani"
},
{
"_id": ObjectId("5daea43a517cf601a7e80a3b"),
"CategoryName":"Kathirikai gothsu"
}
]);
await Recipe.insertMany([
{
"recipeTitle":"Mutton dum biryani",
"Recipetags":["Indian","NonVeg","Lunch"],
"cookTime":"30 Mins",
"recipeCategoryId":[
ObjectId("5dada3c5761bb32a1201d4da"),
ObjectId("5dada3c5761bb32a1201d4db"),
ObjectId("5dada3c5761bb32a1201d4dc")
],
"recipeCuisienId":"Indian",
"recepeType":false,
"availaleStreaming":"TEXT",
"postedOn": new Date(),
"postedBy":"shiva#yopmail.com"
},
{
"recipeTitle":"Mutton Chicken biryani",
"Recipetags":["Indian","NonVeg","Lunch"],
"cookTime":"30 Mins",
"recipeCategoryId":[
ObjectId("5dada3c5761bb32a1201d4da"),
ObjectId("5dada3c5761bb32a1201d4d4"),
ObjectId("5dada3c5761bb32a1201d4dc")
],
"recipeCuisienId":"Indian",
"recepeType":false,
"availaleStreaming":"TEXT",
"postedOn": new Date(),
"postedBy":"shiva#yopmail.com"
}
]);
await User.create({
"emailId":"shiva#yopmail.com",
"fullName":"siva prakash",
"accessToken":"xxxxxxxxxxxxx",
});
const wantedCategories = [
ObjectId("5dada3c5761bb32a1201d4da"),
ObjectId("5dada3c5761bb32a1201d4db")
];
let data = await Recipe.aggregate([
// Match wanted category(ies)
{ "$match": {
"recipeCategoryId": { "$in": wantedCategories }
}},
// Filter the content of the array
{ "$addFields": {
"recipeCategoryId": {
"$filter": {
"input": "$recipeCategoryId",
"cond": {
"$in": [ "$$this", wantedCategories ]
}
}
}
}},
// Lookup the related matching category(ies)
{ "$lookup": {
"from": RecipeCategory.collection.name,
"let": { "recipeCategoryIds": "$recipeCategoryId" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$recipeCategoryIds" ] }
}}
],
"as": "recipeCategoryId"
}},
// Lookup the related user to postedBy
{ "$lookup": {
"from": User.collection.name,
"let": { "postedBy": "$postedBy" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$emailId", "$$postedBy" ] } } }
],
"as": "postedBy"
}},
// postedBy is "singular"
{ "$unwind": "$postedBy" }
]);
log({ data });
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect();
}
})()

MongoDB Aggregate Pipeline query

I'm using mongoDB 3.6 on node.js 8.11.1 and working with MongoDB Node.js Driver.
I have two collections, 'group' and 'user':
group:
[
{
"_id":1,
"groupName":"group1",
"users":[
{
"userId":1,
"isAdmin":"false"
},
{
"userId":2,
"isAdmin":"true"
}
]
},
{
"_id":2,
"groupName":"group2",
"users":[
{
"userId":2,
"isAdmin":"false"
},
{
"userId":3,
"isAdmin":"true"
}
]
}
]
user:
[
{
"_id":1,
"username":"user1",
"firstname":"a",
"lastname":"aa",
"mobileNo":"+1111111"
},
{
"_id":2,
"username":"user2",
"firstname":"b",
"lastname":"bb",
"mobileNo":"+2222222"
},
{
"_id":3,
"username":"user3",
"firstname":"c",
"lastname":"cc",
"mobileNo":"+3333333"
}
]
I need an aggregate to return something like this:
[
{
"_id":1,
"groupName":"group1",
"members":[
{
"isAdmin":"false",
"username":"user1",
"firstname":"a",
"lastname":"aa"
},
{
"isAdmin":"true",
"username":"user2",
"firstname":"b",
"lastname":"bb"
}
]
},
{
"_id":2,
"groupName":"group2",
"members":[
{
"isAdmin":"false",
"username":"user2",
"firstname":"b",
"lastname":"bb"
},
{
"isAdmin":"true",
"username":"user3",
"firstname":"c",
"lastname":"cc"
}
]
}
]
At "members" in result, "isAdmin" return from "users" at group collection and "username", "firstname" and "lastname" came from user collection
Many thanks,
Milad.
You can try below aggregation from mongodb 3.6 and above
db.group.aggregate([
{ "$unwind": "$users" },
{ "$lookup": {
"from": Users.collection.name,
"let": { "userId": "$users.userId", "isAdmin": "$users.isAdmin" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$userId" ] } } },
{ "$project": { "isAdmin": "$$isAdmin", "username": 1, "firstName": 1, "lastName": 1 }}
],
"as": "members"
}},
{ "$unwind": "$members" },
{ "$group": {
"_id": "$_id",
"members": { "$push": "$members" },
"groupName": { "$first": "$groupName" }
}}
])

Lookup when foreignField is in an Array

I want to lookup from an object to a collection where the foreignField key is embedded into an array of objects. I have:
collection "shirts"
{
"_id" : ObjectId("5a797ef0768d8418866eb0f6"),
"name" : "Supermanshirt",
"price" : 9.99,
"flavours" : [
{
"flavId" : ObjectId("5a797f8c768d8418866ebad3"),
"size" : "M",
"color": "white",
},
{
"flavId" : ObjectId("3a797f8c768d8418866eb0f7"),
"size" : "XL",
"color": "red",
},
]
}
collection "basket"
{
"_id" : ObjectId("5a797ef0333d8418866ebabc"),
"basketName" : "Default",
"items" : [
{
"dateAdded" : 1526996879787.0,
"itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
}
],
}
My Query:
basketSchema.aggregate([
{
$match: { $and: [{ _id }, { basketName }]},
},
{
$unwind: '$items',
},
{
$lookup:
{
from: 'shirts',
localField: 'items.itemFlavId',
foreignField: 'flavours.flavId',
as: 'ordered_shirts',
},
},
]).toArray();
my expected result:
[{
"_id" : ObjectId("5a797ef0333d8418866ebabc"),
"basketName" : "Default",
"items" : [
{
"dateAdded" : 1526996879787.0,
"itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
}
],
"ordered_shirts" : [
{
"_id" : ObjectId("5a797ef0768d8418866eb0f6"),
"name" : "Supermanshirt",
"price" : 9.99,
"flavours" : [
{
"flavId" : ObjectId("5a797f8c768d8418866ebad3"),
"size" : "M",
"color": "white",
}
]
}
],
}]
but instead my ordered_shirts array is empty.
How can I use a foreignField if this foreignField is embedded in an array at the other collection?
I am using MongoDB 3.6.4
As commented, it would appear that there is simply something up in your code where you are pointing at the wrong collection. The general case for this is to simply look at the example listing provided below and see what the differences are, since with the data you provide and the correct collection names then your expected result is in fact returned.
Of course where you need to take such a query "after" that initial $lookup stage is not a simple matter. From a structural standpoint, what you have is generally not a great idea since referring "joins" into items within an array means you are always returning data which is not necessarily "related".
There are some ways to combat that, and mostly there is the form of "non-correlated" $lookup introduced with MongoDB 3.6 which can aid in ensuring you are not returning "unnecessary" data in the "join".
I'm working here in the form of "merging" the "sku" detail with the "items" in the basket, so a first form would be:
Optimal MongoDB 3.6
// Store some vars like you have
let _id = ObjectId("5a797ef0333d8418866ebabc"),
basketName = "Default";
// Run non-correlated $lookup
let optimal = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$lookup": {
"from": Shirt.collection.name,
"as": "items",
"let": { "items": "$items" },
"pipeline": [
{ "$match": {
"$expr": {
"$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
}
}},
{ "$project": {
"_id": 0,
"items": {
"$map": {
"input": {
"$filter": {
"input": "$flavours",
"cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
}
},
"in": {
"$mergeObjects": [
{ "$arrayElemAt": [
"$$items",
{ "$indexOfArray": [
"$$items.itemFlavId", "$$this.flavId" ] }
]},
{ "name": "$name", "price": "$price" },
"$$this"
]
}
}
}
}},
{ "$unwind": "$items" },
{ "$replaceRoot": { "newRoot": "$items" } }
]
}}
])
Note that since you are using mongoose to hold details for the models we can use Shirt.collection.name here to read the property from that model with the actual collection name as needed for the $lookup. This helps avoid confusion within the code and also "hard-coding" something like the collection name when it's actually stored somewhere else. In this way should you change the code which registers the "model" in a way which altered the collection name, then this would always retrieve the correct name for use in the pipeline stage.
The main reason you use this form of $lookup with MongoDB 3.6 is because you want to use that "sub-pipeline" to manipulate the foreign collection results "before" they are returned and merged with the parent document. Since we are "merging" the results into the existing "items" array of the basket we use the same field name in argument to "as".
In this form of $lookup you typically still want "related" documents even though it gives you the control to do whatever you want. In this case we can compare the array content from "items" in the parent document which we set as a variable for the pipeline to use with the array under "flavours" in the foreign collection. A logical comparison for the two "sets" of values here where they "intersect" is using the $setIsSubset operator using the $expr so we can compare on a "logical operation".
The main work here is being done in the $project which is simply using $map on the array from the "flavours" array of the foreign document, processed with $filter in comparison to the "items" we passed into the pipeline and essentially re-written in order to "merge" the matched content.
The $filter reduces down the list for consideration to only those which match something present within the "items", and then we can use $indexOfArray and $arrayElemAt in order to extract the detail from the "items" and merge it with each remaining "flavours" entry which matches using the $mergeObjects operator. Noting here that we also take some "parent" detail from the "shirt" as the "name" and "price" fields which are common to the variations in size and color.
Since this is still an "array" within the matched document(s) to the join condition, in order to get a "flat list" of objects suitable for "merged" entries in the resulting "items" of the $lookup we simply apply $unwind, which within the context of matched items left only creates "little" overhead, and $replaceRoot in order to promote the content under that key to the top level.
The result is just the "merged" content listed in the "items" of the basket.
Sub-optimal MongoDB
The alternate approaches are really not that great since all involve returning other "flavours" which do not actually match the items in the basket. This basically involves "post-filtering" the results obtained from the $lookup as opposed to "pre-filtering" which the process above does.
So the next case here would be using methods to manipulate the returned array in order to remove the items which don't actually match:
// Using legacy $lookup
let alternate = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$lookup": {
"from": Shirt.collection.name,
"localField": "items.itemFlavId",
"foreignField": "flavours.flavId",
"as": "ordered_items"
}},
{ "$addFields": {
"items": {
"$let": {
"vars": {
"ordered_items": {
"$reduce": {
"input": {
"$map": {
"input": "$ordered_items",
"as": "o",
"in": {
"$map": {
"input": {
"$filter": {
"input": "$$o.flavours",
"cond": {
"$in": ["$$this.flavId", "$items.itemFlavId"]
}
}
},
"as": "f",
"in": {
"$mergeObjects": [
{ "name": "$$o.name", "price": "$$o.price" },
"$$f"
]
}
}
}
}
},
"initialValue": [],
"in": { "$concatArrays": ["$$value", "$$this"] }
}
}
},
"in": {
"$map": {
"input": "$items",
"in": {
"$mergeObjects": [
"$$this",
{ "$arrayElemAt": [
"$$ordered_items",
{ "$indexOfArray": [
"$$ordered_items.flavId", "$$this.itemFlavId"
]}
]}
]
}
}
}
}
},
"ordered_items": "$$REMOVE"
}}
]);
Here I'm still using some MongoDB 3.6 features, but these are not a "requirement" of the logic involved. The main constraint in this approach is actually the $reduce which requires MongoDB 3.4 or greater.
Using the same "legacy" form of $lookup as you were attempting, we still get the desired results as you display but that of course contains information in the "flavours" that does not match the "items" in the basket. In much the same way as shown in the previous listing we can apply $filter here to remove the items which don't match. The same process here uses that $filter output as the input for $map, which again is doing the same "merge" process as before.
Where the $reduce comes in is because the resulting processing where there is an "array" target from $lookup with documents that themselves contain an "array" of "flavours" is that these arrays need to be "merged" into a single array for further processing. The $reduce simply uses the processed output and performs a $concatArrays on each of the "inner" arrays returned to make these results singular. We already "merged" the content, so this becomes the new "merged" "items".
Older Still $unwind
And of course the final way to present ( even though there are other combinations ) is using $unwind on the arrays and using $group to put it back together:
let old = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$unwind": "$items" },
{ "$lookup": {
"from": Shirt.collection.name,
"localField": "items.itemFlavId",
"foreignField": "flavours.flavId",
"as": "ordered_items"
}},
{ "$unwind": "$ordered_items" },
{ "$unwind": "$ordered_items.flavours" },
{ "$redact": {
"$cond": {
"if": {
"$eq": [
"$items.itemFlavId",
"$ordered_items.flavours.flavId"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"basketName": { "$first": "$basketName" },
"items": {
"$push": {
"dateAdded": "$items.dateAdded",
"itemFlavId": "$items.itemFlavId",
"name": "$ordered_items.name",
"price": "$ordered_items.price",
"flavId": "$ordered_items.flavours.flavId",
"size": "$ordered_items.flavours.size",
"color": "$ordered_items.flavours.color"
}
}
}}
]);
Most of this should be pretty self explanatory as $unwind is simply a tool to "flatten" array content into singular document entries. In order to just get the results we want we can use $redact to compare the two fields. Using MongoDB 3.6 you "could" use $expr within a $match here:
{ "$match": {
"$expr": {
"$eq": [
"$items.itemFlavId",
"$ordered_items.flavours.flavId"
]
}
}}
But when it comes down to it, if you have MongoDB 3.6 with it's other features then $unwind is the wrong thing to do here due to all the overhead it will actually add.
So all that really happens is you $lookup then "flatten" the documents and finally $group all related detail together using $push to recreate the "items" in the basket. It "looks simple" and is probably the most easy form to understand, however "simplicity" does not equal "performance" and this would be pretty brutal to use in a real world use case.
Summary
That should cover the explanation of the things you need to do when working with "joins" that are going to compare items within arrays. This probably should lead you on the path of realizing this is not really a great idea and it would be far better to keep your "skus" listed "separately" rather than listing them all related under a single "item".
It also should in part be a lesson that "joins" in general are not a great idea with MongoDB. You really only should define such relations where they are "absolutely necessary". In such a case of "details for items in a basket", then contrary to traditional RDBMS patterns it would actually be far better in terms of performance to simply "embed" that detail from the start. In that way you don't need complicated join conditions just to get a result, which might have saved "a few bytes" in storage but is taking a lot more time than what should have been a simple request for the basket with all the detail already "embedded". That really should be the primary reason why you are using something like MongoDB in the first place.
So if you have to do it, then really you should be sticking with the first form since where you have the available features to use then use them best to their advantage. Whilst other approaches may seem easier, it won't help the application performance, and of course best performance would be embedding to begin with.
A full listing follows for demonstration of the above discussed methods and for basic comparison to prove that the provided data does in fact "join" as long as the other parts of the application set-up are working as they should be. So a model on "how it should be done" in addition to demonstrating the full concepts.
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/basket';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const basketItemSchema = new Schema({
dateAdded: { type: Number, default: Date.now() },
itemFlavId: { type: Schema.Types.ObjectId }
},{ _id: false });
const basketSchema = new Schema({
basketName: String,
items: [basketItemSchema]
});
const flavourSchema = new Schema({
flavId: { type: Schema.Types.ObjectId },
size: String,
color: String
},{ _id: false });
const shirtSchema = new Schema({
name: String,
price: Number,
flavours: [flavourSchema]
});
const Basket = mongoose.model('Basket', basketSchema);
const Shirt = mongoose.model('Shirt', shirtSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// set up data for test
await Basket.create({
_id: ObjectId("5a797ef0333d8418866ebabc"),
basketName: "Default",
items: [
{
dateAdded: 1526996879787.0,
itemFlavId: ObjectId("5a797f8c768d8418866ebad3")
}
]
});
await Shirt.create({
_id: ObjectId("5a797ef0768d8418866eb0f6"),
name: "Supermanshirt",
price: 9.99,
flavours: [
{
flavId: ObjectId("5a797f8c768d8418866ebad3"),
size: "M",
color: "white"
},
{
flavId: ObjectId("3a797f8c768d8418866eb0f7"),
size: "XL",
color: "red"
}
]
});
// Store some vars like you have
let _id = ObjectId("5a797ef0333d8418866ebabc"),
basketName = "Default";
// Run non-correlated $lookup
let optimal = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$lookup": {
"from": Shirt.collection.name,
"as": "items",
"let": { "items": "$items" },
"pipeline": [
{ "$match": {
"$expr": {
"$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
}
}},
{ "$project": {
"_id": 0,
"items": {
"$map": {
"input": {
"$filter": {
"input": "$flavours",
"cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
}
},
"in": {
"$mergeObjects": [
{ "$arrayElemAt": [
"$$items",
{ "$indexOfArray": [
"$$items.itemFlavId", "$$this.flavId" ] }
]},
{ "name": "$name", "price": "$price" },
"$$this"
]
}
}
}
}},
{ "$unwind": "$items" },
{ "$replaceRoot": { "newRoot": "$items" } }
]
}}
])
log(optimal);
// Using legacy $lookup
let alternate = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$lookup": {
"from": Shirt.collection.name,
"localField": "items.itemFlavId",
"foreignField": "flavours.flavId",
"as": "ordered_items"
}},
{ "$addFields": {
"items": {
"$let": {
"vars": {
"ordered_items": {
"$reduce": {
"input": {
"$map": {
"input": "$ordered_items",
"as": "o",
"in": {
"$map": {
"input": {
"$filter": {
"input": "$$o.flavours",
"cond": {
"$in": ["$$this.flavId", "$items.itemFlavId"]
}
}
},
"as": "f",
"in": {
"$mergeObjects": [
{ "name": "$$o.name", "price": "$$o.price" },
"$$f"
]
}
}
}
}
},
"initialValue": [],
"in": { "$concatArrays": ["$$value", "$$this"] }
}
}
},
"in": {
"$map": {
"input": "$items",
"in": {
"$mergeObjects": [
"$$this",
{ "$arrayElemAt": [
"$$ordered_items",
{ "$indexOfArray": [
"$$ordered_items.flavId", "$$this.itemFlavId"
]}
]}
]
}
}
}
}
},
"ordered_items": "$$REMOVE"
}}
]);
log(alternate);
// Or really old style
let old = await Basket.aggregate([
{ "$match": { _id, basketName } },
{ "$unwind": "$items" },
{ "$lookup": {
"from": Shirt.collection.name,
"localField": "items.itemFlavId",
"foreignField": "flavours.flavId",
"as": "ordered_items"
}},
{ "$unwind": "$ordered_items" },
{ "$unwind": "$ordered_items.flavours" },
{ "$redact": {
"$cond": {
"if": {
"$eq": [
"$items.itemFlavId",
"$ordered_items.flavours.flavId"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"basketName": { "$first": "$basketName" },
"items": {
"$push": {
"dateAdded": "$items.dateAdded",
"itemFlavId": "$items.itemFlavId",
"name": "$ordered_items.name",
"price": "$ordered_items.price",
"flavId": "$ordered_items.flavours.flavId",
"size": "$ordered_items.flavours.size",
"color": "$ordered_items.flavours.color"
}
}
}}
]);
log(old);
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
And sample output as:
Mongoose: baskets.remove({}, {})
Mongoose: shirts.remove({}, {})
Mongoose: baskets.insertOne({ _id: ObjectId("5a797ef0333d8418866ebabc"), basketName: 'Default', items: [ { dateAdded: 1526996879787, itemFlavId: ObjectId("5a797f8c768d8418866ebad3") } ], __v: 0 })
Mongoose: shirts.insertOne({ _id: ObjectId("5a797ef0768d8418866eb0f6"), name: 'Supermanshirt', price: 9.99, flavours: [ { flavId: ObjectId("5a797f8c768d8418866ebad3"), size: 'M', color: 'white' }, { flavId: ObjectId("3a797f8c768d8418866eb0f7"), size: 'XL', color: 'red' } ], __v: 0 })
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', as: 'items', let: { items: '$items' }, pipeline: [ { '$match': { '$expr': { '$setIsSubset': [ '$$items.itemflavId', '$flavours.flavId' ] } } }, { '$project': { _id: 0, items: { '$map': { input: { '$filter': { input: '$flavours', cond: { '$in': [Array] } } }, in: { '$mergeObjects': [ { '$arrayElemAt': [Array] }, { name: '$name', price: '$price' }, '$$this' ] } } } } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ] } } ], {})
[
{
"_id": "5a797ef0333d8418866ebabc",
"basketName": "Default",
"items": [
{
"dateAdded": 1526996879787,
"itemFlavId": "5a797f8c768d8418866ebad3",
"name": "Supermanshirt",
"price": 9.99,
"flavId": "5a797f8c768d8418866ebad3",
"size": "M",
"color": "white"
}
],
"__v": 0
}
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$addFields': { items: { '$let': { vars: { ordered_items: { '$reduce': { input: { '$map': { input: '$ordered_items', as: 'o', in: { '$map': [Object] } } }, initialValue: [], in: { '$concatArrays': [ '$$value', '$$this' ] } } } }, in: { '$map': { input: '$items', in: { '$mergeObjects': [ '$$this', { '$arrayElemAt': [ '$$ordered_items', [Object] ] } ] } } } } }, ordered_items: '$$REMOVE' } } ], {})
[
{
"_id": "5a797ef0333d8418866ebabc",
"basketName": "Default",
"items": [
{
"dateAdded": 1526996879787,
"itemFlavId": "5a797f8c768d8418866ebad3",
"name": "Supermanshirt",
"price": 9.99,
"flavId": "5a797f8c768d8418866ebad3",
"size": "M",
"color": "white"
}
],
"__v": 0
}
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$unwind': '$items' }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$unwind': '$ordered_items' }, { '$unwind': '$ordered_items.flavours' }, { '$redact': { '$cond': { if: { '$eq': [ '$items.itemFlavId', '$ordered_items.flavours.flavId' ] }, then: '$$KEEP', else: '$$PRUNE' } } }, { '$group': { _id: '$_id', basketName: { '$first': '$basketName' }, items: { '$push': { dateAdded: '$items.dateAdded', itemFlavId: '$items.itemFlavId', name: '$ordered_items.name', price: '$ordered_items.price', flavId: '$ordered_items.flavours.flavId', size: '$ordered_items.flavours.size', color: '$ordered_items.flavours.color' } } } } ], {})
[
{
"_id": "5a797ef0333d8418866ebabc",
"basketName": "Default",
"items": [
{
"dateAdded": 1526996879787,
"itemFlavId": "5a797f8c768d8418866ebad3",
"name": "Supermanshirt",
"price": 9.99,
"flavId": "5a797f8c768d8418866ebad3",
"size": "M",
"color": "white"
}
]
}
]

Resources