MongoDB Aggregation of a user and his groups - node.js

I have 2 collections, "user" and "group" and I wanna return an object of a user and his groups.
The user is something like
{
"_id": "5dea1ad0df42010984d1036f",
// more info
}
and the group
{
"_id": "5dea1ad0df42010984d1566a",
"members": [
{
"_id": "5dea1ad0df42010984d1036f",
"fullName": "Example",
"img":"https://example.com",
},
// more objects like that
]
}
Still didn't find a proper way of doing it, I'd be happy if someone can guide me/explain!

You can use $lookup with custom pipeline:
db.user.aggregate([
{
$lookup: {
from: "group",
let: { user_id: "$_id" },
pipeline: [
{ $match: { $expr: { $in: [ "$$user_id", "$members._id" ] } } }
],
as: "groups"
}
}
])
Mongo Playground

Related

Mongodb and nodejs find and filter nested objects

Firstly this is my Workspaces collection
[
{
"WorkspaceId": "er890we8rw98ro9we8rjower",
"WorkspaceTitle": "My First Workspace",
"WorkspaceOwner": "user1",
"Pages": [
{
"PageId": "a1sd32as7d841a23sd",
"PageMembers": [
{
"MemberId": "user1",
"MemberName": "John",
"MemberAvatar": "https://example.com/jKLa29Wqy",
"MemberAccess": "can edit"
},
{
"MemberId": "user2",
"MemberName": "Margot",
"MemberAvatar": "https://example.com/wKKLSAqy",
"MemberAccess": "can edit"
},
{
"MemberId": "user3",
"MemberName": "Silvia",
"MemberAvatar": "https://example.com/wKKLSAqy",
"MemberAccess": "can edit"
},
...
]
}
]
}
]
I wanr to print specific nested objects from document. I need to get a specific document inside my collection like this.
{
"WorkspaceId": "er890we8rw98ro9we8rjower",
"WorkspaceTitle": "My First Workspace",
"WorkspaceOwner": "user1",
"Pages": [
{
"PageId": "a1sd32as7d841a23sd",
"PageMembers": [
{
"MemberId": "user1",
"MemberName": "John",
"MemberAvatar": "https://example.com/jKLa29Wqy",
"MemberAccess": "can edit"
}
]
}
]
}
But when I run this script, my api prints all of collection. How can I achieve this?
Below is the query I am using:
db.findOne({ $or: [{ "WorkspaceOwner": member_id }, { "Pages.PageMembers.MemberId": member_id }] }, function (err, result) {
res.status(200).json(result);
});
You can achieve by using unwind and filter aggregation.
db.collection.aggregate([
{
"$unwind": "$Pages"
},
{
$addFields: {
"Pages.PageMembers": {
$filter: {
input: "$Pages.PageMembers",
as: "temp",
cond: {
$in: [
"$$temp.MemberId",
[
"user1"
]
]
}
}
}
}
}
])
Playground

Lookup based on match result

I have collection name Services:
[
{
"_id": "61dad1d21aa077c61b7bc2aa",
"name": "HomeMaintenance",
"subServices": [
"61dacb86cb94917c1edcea8f",
"61dad5812881410ba441c401"
],
},
{
"_id": "61dad60b2881410ba441c40e",
"name": "HomeMaintenance",
"subServices": [],
}
]
in another hand I have a subServices Collection like this :
[
{
"_id": "61dacb86cb94917c1edcea8f",
"name": "something",
"title": "something else",
"imageUrl": "",
"__v": 0,
"service": "61dad1d21aa077c61b7bc2aa"
},
{
"_id": "61dad5812881410ba441c401",
"name": "Plumbing",
"title": "Plumbing",
"imageUrl": "",
"__v": 0,
"service": "61dad1d21aa077c61b7bc2aa"
}
]
I came up with a solution with two queries like this
const requestedService = (serviceId)=>{
return servicesModel.findById(id);
};
const ids= requestedService.subServices
const subServicesList = (ids) => {
return subServicesModel.find({
_id: {
$in: ids,
},
});
};
which works perfectly fine, I was wondering is there any way to do these queries with one aggregation pipeline with lookup stage, first find the main services from service collection and then from subServices collection find that subServices of service
something like this
const result = await servicesModel.aggregate([
{
$match: { _id: ObjectId(id) },
},
{
$lookup: {
from: "sub_services",
let: { pid: "$_id" },
pipeline: [
{
$match: {
$expr: {
$in: ["$$pid" //>> id in sub_services modal , //>> "array which we get from match" ],
},
},
},
],
as: "subServices",
},
},
]);
The let is used for declaring the variable from the left document.
Specifies variables to use in the pipeline stages. Use the variable expressions to access the fields from the joined collection's documents that are input to the pipeline.
db.services.aggregate([
{
$match: {
_id: ObjectId(id)
},
},
{
$lookup: {
from: "sub_services",
let: {
subServices: "$subServices"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$_id",
"$$subServices"
]
},
},
},
],
as: "subServices",
},
},
])
Sample Mongo Playground

Return document if only sub-document elements matched within a nested array

The main collection is User, and we have a User profile collection which having experience details and other stuff. Also, we have a Skill collection.
USER
[{
"_id": "5f1eef8ec68d306fbbf13b0f",
"name": "John Davis",
"email": "John#gmail.com",
"__v": 0
},
{
"_id": "9q1eef8ec68d306fbbf13bh6",
"name": "Mik Luca",
"email": "Mik#gmail.com",
"__v": 0
}]
User profile
[{
"_id": "5f1eef8ec68d306fbbf13b10",
"other_skills": [
null
],
"user_id": "5f1eef8ec68d306fbbf13b0f",
"phone_number": "1234569870",
"location": "5f16b72617fee02922688751",
"primary_skills": [
{
"_id": "5f32635cf764cc40447503a6",
"years": 1,
"skill_id": "5f0da75907a96c3040b3667d"
}
]
},
{
"_id": "5f1eef8ec68d306fbbf13b10",
"other_skills": [
null
],
"user_id": "9q1eef8ec68d306fbbf13bh6",
"phone_number": "1234569870",
"location": "5f16b72617fee02922688751",
"primary_skills": [
{
"_id": "6s0da75907a96c3040b36690",
"years": 1,
"skill_id": "5f0da75907a96c3040b3667d"
}
]
}]
Skill
[{
"_id": "5f0da75907a96c3040b3667d",
"skill": "Mongo"
},
{
"_id": "6s0da75907a96c3040b36690",
"skill": "Node"
}]
I need to list the users with their user profile info and need to filter with skills as well
I have tried
db.getCollection("users").aggregate(
[
{
"$project" : {
"_id" : NumberInt(0),
"users" : "$$ROOT"
}
},
{
"$lookup" : {
"localField" : "users._id",
"from" : "userprofiles",
"foreignField" : "user_id",
"as" : "userprofiles"
}
},
{
"$unwind" : {
"path" : "$userprofiles",
"preserveNullAndEmptyArrays" : true
}
},
{
"$lookup" : {
"localField" : "userprofiles.primary_skills.skill_id",
"from" : "skills",
"foreignField" : "_id",
"as" : "skills"
}
},
{
"$unwind" : {
"path" : "$skills",
"preserveNullAndEmptyArrays" : true
}
},
{
"$match" : {
"skills._id" : ObjectId("5f0dce8d07a96c3040b36687")
}
}
],
{
"allowDiskUse" : true
}
);
But not getting the proper results.
How can I populate the user profile and skill information with the User list and filter the user list with Skill ids?
Greetings and thanks.
You can match filters inside lookup using lookup with pipeline,
$lookup with userProfile collection
pipelines $match to match profile id
other filters for profile like skill_id match here
$unwind deconstruct primary_skills array because we are going to lookup with skill_id
$lookup will skills collection
$unwind deconstruct primary_skills.skill_id array because we need it as object
$grpup reconstruct primary_skills array
$match if userProfiles not equal to empty []
db.users.aggregate([
{
$lookup: {
from: "usersProfile",
let: { id: "$_id" },
as: "userProfiles",
pipeline: [
{
$match: {
$expr: { $eq: ["$$id", "$user_id"] },
// match here user profile filters
"primary_skills.skill_id": "5f0da75907a96c3040b3667d"
}
},
{ $unwind: "$primary_skills" },
{
$lookup: {
from: "skills",
localField: "primary_skills.skill_id",
foreignField: "_id",
as: "primary_skills.skill_id"
}
},
{ $unwind: "$primary_skills.skill_id" },
{
$group: {
_id: "$_id",
other_skills: { $first: "$other_skills" },
phone_number: { $first: "$phone_number" },
location: { $first: "$location" },
primary_skills: {
$push: {
_id: "$primary_skills._id",
skill: "$primary_skills.skill_id.skill",
years: "$primary_skills.years"
}
}
}
}
]
}
},
{ $match: { userProfiles: { $ne: [] } } }
])
Playground

Can't join collections with ids in mongodb

I am trying to get all requests of a user from the request collection based on request status. I am trying to lookup the collection but it doesn't work. Is there any solution to work it out.
Here is my code:
Users.aggregate([
{
$lookup: {
from: 'requests',
let: {userId: '$userId', status: '$status'},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$_id', '$$userId'] },
{ $eq: ['$$status', 1] }
]
},
}
}
],
as: 'requests'
}
}
]).exec()
I think { $eq: ['$_id', '$$userId'] } is not working. I tried using $toObjectId but still same result.
Here is test data for users:
{
"_id": {
"$oid": "5f1c0112ad207a13308a3fea"
},
"createDate": {
"$date": "2020-07-25T09:52:58.678Z"
},
"userRole": 10,
"status": 1,
"fullName": "Test Name",
"email": "test.name#mailinator.com",
"password": "$2b$10$HQN//qFTQKW8tBnf7G0OV.Uta0zNbxd1hPlGVwvLp5CVIf49Y5PNW",
"__v": 0,
"profileImage": "1595957619296.jpg"
}
And test request data:
{
"_id": {
"$oid": "5f2178c578153018ca5d79e8"
},
"request": "This is a demo request.",
"userId": {
"$oid": "5f1c0112ad207a13308a3fea"
},
"createDate": {
"$date": "2020-07-28T18:30:00.000Z"
},
"status": 1
}
There are few fixes in your query,
$match your user document status is 1, if you don't want then your can exclude,
db.users.aggregate([
{
$match: {
status: 1
}
},
$lookup with requests
let userId it is from user collection so add user collection _id so its corrected,
no need to create status variable because we already checked condition for in above
{
$lookup: {
from: "requests",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
$eq check first is requests collection $userId and second is that we have created variable above in let and use $$userId because its reference to main collection users let variables
{
$eq: [
"$userId",
"$$userId"
]
},
second $eq check $status is 1 in requests collection, and you did it in user collection we we have already used in above $match condition
{
$eq: [
"$status",
1
]
}
]
}
}
}
],
as: "requests"
}
}
])
Have divided in parts for explanation purpose you can merge it as they are in sequence,
Working Playground: https://mongoplayground.net/p/THU6HeyqMN4

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