Pagination in aggregation and also used $project in mongoose - node.js

i want to show user list of object with projection and also want to show the total page i want output like this
{
"Success": true,
"message": " Fetched post comment successfully.",
"data": {
"docs": [
{
"_id": "60101fcc4077e698facd63aa",
"commentDetail": {
"hasReply": false,
"likeCount": 0,
"angryCount": 0,
"favCount": 0,
"totalReaction": 0,
"description": "four comment",
"media": null,
"date": "2021-01-26T13:57:32.220Z",
"action": [],
"commentReplies": []
},
"userID": "5f5238b5458b7c63a477bf87",
"postID": "5fb7a19bcae255415e99781b",
"commentID": "60101fcb4077e698facd63a9",
},
{
"_id": "60101fcc4077e698facd63aa",
"commentDetail": {
"hasReply": false,
"likeCount": 0,
"angryCount": 0,
"favCount": 0,
"totalReaction": 0,
"description": "four comment",
"media": null,
"date": "2021-01-26T13:57:32.220Z",
"action": [],
"commentReplies": []
},
"userID": "5f5238b5458b7c63a477bf87",
"postID": "5fb7a19bcae255415e99781b",
"commentID": "60101fcb4077e698facd63a9",
}
],
"count": 1
}
}
i write this query
getPostAllComment = await this.comment.aggregate([
{ $match: { postID: ObjectId(getPostCommentDTO.postID) } },
{ $sort: { createdAt: 1 } }, //sort on created At
{ $skip: (parseInt(getPostCommentDTO.pageNum) - 1) * parseInt(getPostCommentDTO.pageSize) },
{ $limit: parseInt(getPostCommentDTO.pageSize) },
{
$group: {
_id: "$postID",
docs: { $push: "$$ROOT" },
count: { $sum: 1 },
},
},
{ $project: { _id: 0 }
}
it give me output like above which i am expecting but my $project is not working here.so how can i show output exactly like above with total page count field at root level not in each document with projection.i already tried to used $facet but it not give me output like i above explain

I get my desire result by this query
getPostAllComment = await this.comment.aggregate([
{ $match: { postID: ObjectId(getPostCommentDTO.postID) } },
{
$group: {
_id: null,
collection: { $push: "$$ROOT" },
count: { $sum: 1 },
},
},
{ $unwind: "$collection" },
{ $sort: { createdAt: 1 } }, //sort on created At
{ $skip: (parseInt(getPostCommentDTO.pageNum) - 1) * parseInt(getPostCommentDTO.pageSize) },
{ $limit: parseInt(getPostCommentDTO.pageSize) },
{
$project: {
hasReply: "$collection.commentDetail.hasReply",
likeCount: "$collection.commentDetail.likeCount",
angryCount: "$collection.commentDetail.angryCount",
favCount: "$collection.commentDetail.favCount",
totalReaction: "$collection.commentDetail.totalReaction",
date: "$collection.commentDetail.date",
media: "$collection.commentDetail.media",
description: "$collection.commentDetail.description",
action: "$collection.commentDetail.action",
recentCommentReplies: "$collection.commentDetail.commentReplies",
userProfilePicture: "$collection.userProfilePicture",
userProfilePictureThumbnail: "$collection.userProfilePictureThumbnail",
userName: "$collection.userName",
userID: "$collection.userID",
postID: "$collection.postID",
commentID: "$collection.commentID",
createdAt: "$collection.createdAt",
updatedAt: "$collection.updatedAt",
count: "$count",
},
},
{
$group: {
_id: "$postID",
docs: { $push: "$$ROOT" },
total: { $first: "$count" },
},
},
{ $project: { "docs.count": 0, _id: 0 } },
])
return getPostAllComment[0]

Related

How to calculate the percentage using facet in MongoDB?

I am calculating the notification percentage in my app for tracking some statistics.
My Collection:
[
{
_id: "123",
status: "seen",
userId: "589"
},
{
_id: "223",
status: "seen",
userId: "589"
},
{
_id: "474",
status: "unseen",
userId: "589"
},
{
_id: "875",
status: "seen",
userId: "112"
},
{
_id: "891",
status: "unseen",
userId: "112"
}
]
Expected Result:
Here we can see that, UserId - 589 has received 3 notifications out of which 2 are seen. So the calculation is (totalNumOfSeen/totalNumOfNoticationsSent) * 100
[{
userId: "589",
notificationPercentage : 66.66
},{
userId: "112",
notificationPercentage : 50
}]
I am using a facet for grouping and matching but that is returning me an array of object and I am not getting how to perform divide on this.
My Query:
db.collection.aggregate([
{
$facet: {
totalNumOfSeen: [
{
$match: {
userId: "589",
status: "seen"
}
},
{
$group: {
_id: "$userId",
totalNumOfSeen: {
$sum: 1
}
}
}
],
totalNumOfNoticationsSent: [
{
$match: {
userId: "589",
}
},
{
$group: {
_id: "$userId",
totalNumOfNoticationsSent: {
$sum: 1
}
}
}
]
}
}
])
The Above Query is giving me the below Result:
[
{
"totalNumOfNoticationsSent": [
{
"_id": "589",
"totalNumOfNoticationsSent": 3
}
],
"totalNumOfSeen": [
{
"_id": "589",
"totalNumOfSeen": 2
}
]
}
]
MongoPlayground - https://mongoplayground.net/p/jHn2ZlshgDL
Now I need to add one more field as notificationPercentage and calculate the notification percentage based on the above facet result. Really appreciate the help.
You can try,
$group by userId and get totalSeen count using $cond if status is seen, get total count of notification using $sum,
$project to show required fields, and calculate percentage using $divide and $multiply
db.collection.aggregate([
{
$group: {
_id: "$userId",
totalSeen: {
$sum: { $cond: [{ $eq: ["$status", "seen"] }, 1, 0] }
},
total: { $sum: 1 }
}
},
{
$project: {
_id: 0,
userId: "$_id",
notificationPercentage: {
$multiply: [{ $divide: ["$totalSeen", "$total"] }, 100]
}
}
}
])
Playground

How to calculate frequency of element for two array in same document with arregation in Mongodb

I want to calculate the frequency of tags from correctags and wrongtags from a different document on the basics of username there are multiple documents like below one
required data must have name of each student with there correct and wrong tags name with corresponding frequency of each different tag
I reach till here in my node js code,just getting correct tags correctly
Recently I started aggregation in MongoDB please guide me from where to study this
[
{
'$match': {
'examid': new ObjectId('5f417a52ca1a8428a8eb5cdb')
}
}, {
'$unwind': {
'path': '$correcttags'
}
}, {
'$group': {
'_id': {
'id': '$username',
'correctTag': '$correcttags',
'wrongTag': '$wrongtags'
},
'count': {
'$sum': 1
}
}
}, {
'$group': {
'_id': '$_id.id',
'correct': {
'$push': {
'c': '$_id.correctTag',
'cf': '$count'
}
},
'wrong': {
'$push': {
'w': '$_id.wrongTag',
'wf': '$count'
}
}
}
}
]
Required Information from each student
{
"total":7,
"math":0,
"chemistry":0,
"physics":7,
"correcttags":[
{
"name":" Communications system",
"fr":2
}
],
"wrongtags":[
{
"name":" Communications system",
"fr":1
}
],
"username":"dhirav"
}
that's is my document for each student
{
"_id":{
"$oid":"5f3e7b3958c08f1bd897cad9"
},
"total":7,
"math":0,
"chemistry":0,
"physics":7,
"correcttags":[
" Communications system"," Communications system"
],
"wrongtags":[
" Communications system",
],
"examid":{
"$oid":"5f3b61414d9b8a27ccdd3c6b"
},
"user":{
"$oid":"5f189baf1e7fb81ce09fbb5f"
},
"username":"dhirav",
}
You can do this using $unwind and $group,
$match your conditons
$unwind deconstruct correcttags array
$group by _id and correcttags and do sum
$group by _id and reconstruct correcttags array, its done
db.collection.aggregate([
{ $match: { _id: ObjectId("5f3e7b3958c08f1bd897cad9") } },
{ $unwind: "$correcttags" },
{
$group: {
_id: { _id: "$_id", correcttags: "$correcttags" },
correcttags_fr: { $sum: 1 },
total: { $first: "$total" },
math: { $first: "$math" },
chemistry: { $first: "chemistry" },
physics: { $first: "$physics" },
wrongtags: { $first: "$wrongtags" },
username: { $first: "$username" }
}
},
{
$group: {
_id: "$_id._id",
correcttags: { $push: { name: "$_id.correcttags", fr: "$correcttags_fr" } },
total: { $first: "$total" },
math: { $first: "$math" },
chemistry: { $first: "chemistry" },
physics: { $first: "$physics" },
wrongtags: { $first: "$wrongtags" },
username: { $first: "$username" }
}
},
repeat above same pipelines for wrongtags array
{ $unwind: "$wrongtags" },
{
$group: {
_id: { _id: "$_id", wrongtags: "$wrongtags" },
wrongtags_fr: { $sum: 1 },
total: { $first: "$total" },
math: { $first: "$math" },
chemistry: { $first: "chemistry" },
physics: { $first: "$physics" },
correcttags: { $first: "$correcttags" },
username: { $first: "$username" }
}
},
{
$group: {
_id: "$_id._id",
wrongtags: { $push: { name: "$_id.wrongtags", fr: "$wrongtags_fr" } },
total: { $first: "$total" },
math: { $first: "$math" },
chemistry: { $first: "chemistry" },
physics: { $first: "$physics" },
correcttags: { $first: "$correcttags" },
username: { $first: "$username" }
}
}
])
Playground

Mondogb $sum not working with SchemaTypes.Double

i'm fairly new to MongoDB and Mongoose and i'm working on a bug here. Apparently $sum is not working with a field whose type is SchemaTypes.Double. This double type is available thanks to a package called mongoose-double.
I don't know if MongoDB doesn't support Double so that's why we have this package, or is this because of MongoDB Version (it's on 3.6 AFAIK). But anyways, here's the code:
Schedule.aggregate([{
$match: findTerm
},
{
$facet: {
totalizer: [{
$group: {
_id: '$store',
totalServices: {
$sum: 1
},
totalValue: {
$sum: '$value'
},
totalComission: {
$sum: '$comissionValue'
}
}
}
],
data: [{
$project: {
'employee.name': 1,
'customer.name': 1,
'service.name': 1,
'info.channel': 1,
value: 1,
scheduleDate: 1,
scheduleStart: 1,
scheduleEnd: 1,
comissionValue: 1,
status: 1,
paymentMethod: 1
}
},
{
$sort: sort
},
{
$skip: req.body.limit * req.body.page
},
{
$limit: req.body.limit
}
]
}
}
]).exec((e, response) => {
if (e) {
// handle error
}
res.status(200).send(response[0]);
});
This findTerm is sent by the frontend app and has this format:
{ store: '5b16cceb56a44e2f6cd0324b',
status: { '$in': [ 0, 1, 2, 3 ] },
paymentMethod: { '$in': [ 0, 1, 2, 3, 4, 5 ] },
'info.channel': { '$in': [ 'app', 'admin' ] },
scheduleStart: { '$gte': '2019-11-01 00:00' },
scheduleEnd: { '$lte': '2020-03-31 23:59' }
}
My comissionValue field is in the root of my Schedule Schema:
comissionValue: {
type: SchemaTypes.Double,
default: 0
},
But my result is the following, as shown in my console.log in the frontend
As you can see my totalComission inside my totalizer is null, but my first object inside data has a comissionValue of 0.6.
How can i kno what's wrong here? I've tried different combinations of $facet, filtering only Schedules that has a comissionValue not equal 0 and null, but i only got a result of 0 for totalComission.
EDIT
Here's some sample data:
A Schedule object:
customer: {
id: "5e41a7ba11340930742aa689",
name: "renan lima",
phone: "+5511999999999",
email: "sampleemail#gmail.com",
doc: "00000000000",
avatar: null
},
employee: {
id: "5b16cebd29bcf613f02b6fb4",
name: "Anderson Zanardi",
avatar: "anderson.jpg"
},
service: {
noValue: false,
filters: [],
id: "5b637acd634e14086c9a3aea",
name: "Barba Masculina"
},
info: {
channel: "app",
id: "5e41a7ba11340930742aa689",
name: "renan lima"
},
comissionType: null,
comissionValue: 0,
paymentMethod: 0,
_id: "5e41a7c011340930742aa68a",
store: "5b16cceb56a44e2f6cd0324b",
scheduleDate: "2020-03-16T15:00:00.000Z",
scheduleStart: "2020-03-16 09:00",
scheduleEnd: "2020-03-16 09:30",
status: 2,
value: 30,
color: "blue",
logStatus: [],
__v: 0,
created: "2020-02-10T18:58:08.845Z",
updated: "2020-02-10T18:58:08.845Z"
My response received for the Schedule.aggregate:
{
"totalizer": [{
"_id": null,
"storesCount": [{
"store": "5b16cceb56a44e2f6cd0324b",
"count": 12
}],
"totalValue": 410.5,
"totalServices": 12,
"totalComission": 75
}],
"data": [{
"_id": "5e5d04dcb4a2f42204598ebf",
"service": {
"name": "Outros"
},
"info": {
"channel": "admin"
},
"comissionValue": 0,
"paymentMethod": 0,
"customer": {
"name": "teste"
},
"employee": {
"name": "Gabriel Barreto"
},
"scheduleDate": "2020-03-02T13:06:00.000Z",
"scheduleStart": "2020-03-02 10:06",
"scheduleEnd": "2020-03-02 10:36",
"status": 0,
"value": null
}]
}
Here the comission is 75, i don't know if it's because of the scheduleStart and scheduleDate in the findTerm on my $match that this time is starting at 2020-03-01 and ends at 2020-03-31 and in that range there's 3 schedules with 25 of comission.
Maybe my pagination is making it return null? Since i need it to sum all my comission for a given start/end range, even if in a certain page it doesn't have a comission.
EDIT 2
I added a sample data in Mongo Playground, the schedule array in the configuration column matchs the query used in the $match property on the query column.
Here's the link: https://mongoplayground.net/p/nmyAsY4g7LS

MongoDB aggregate lookup match not working the same without lookup

Can you please help? I'm trying to aggregate data over the past 12 months by both ALL publication data specific to a certain publisher and per publication to return a yearly graph data analysis based on the subscription type.
Here's a snapshot of the Subscriber model:
const SubscriberSchema = new Schema({
publication: { type: Schema.Types.ObjectId, ref: "publicationcollection" },
subType: { type: String }, // Print, Digital, Bundle
subStartDate: { type: Date },
subEndDate: { type: Date },
});
Here's some data for the reader (subscriber) collection:
{
_id: ObjectId("5dc14d3fc86c165ed48b6872"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-20T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6871"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6870"),
publication: ObjectId("5d89db9d82273f1d18970deb"),
subStartDate: "2019-11-18T00:00:00.000Z",
subtype: "digital"
},
{
_id: ObjectId("5dc14d3fc86c165ed48b6869"),
publication: ObjectId("5d8b36c3148c1e5aec64662c"),
subStartDate: "2019-11-19T00:00:00.000Z",
subtype: "print"
}
The publication model has plenty of fields but the _id and user fields are the only point of reference in the following queries.
Here's some data for the publication collection:
// Should use
{ "_id": {
"$oid": "5d8b36c3148c1e5aec64662c"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Should use
{ "_id": {
"$oid": "5d89db9d82273f1d18970deb"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": false
},
// Shouldn't use as deleted === true
{ "_id": {
"$oid": "5d89db9d82273f1d18970dec"
},
"user": {
"$oid": "5d24bbd89f09024590db9dcd"
},
"isDeleted": true
},
// Shouldn't use as different user ID
{ "_id": {
"$oid": "5d89db9d82273f1d18970dfc"
},
"user": {
"$oid": "5d24bbd89f09024590db9efd"
},
"isDeleted": true
}
When I do a lookup on a publication ID with the following, I'm getting perfect results:
Subscriber.aggregate([
{
$match: {
$and: [
{ 'publication': mongoose.Types.ObjectId(req.params.id) },
],
"$expr": { "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
}
},
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$subType" },
count: { $sum: 1 }
},
},
{
/* sort descending (latest subscriptions first) */
$sort: {
'_id.year': -1,
'_id.month': -1
}
},
{
$limit: 100,
},
])
However, when I want to receive data from the readercollections (Subscriber Model) for ALL year data, I'm not getting the desired results (if any) from all of the things I'm trying (I'm posting the best attempt result below):
Publication.aggregate([
{
$match:
{
user: mongoose.Types.ObjectId(id),
isDeleted: false
}
},
{
$project: {
_id: 1,
}
},
{
$lookup: {
from: "readercollections",
let: { "id": "$_id" },
pipeline: [
{
$match:
{
$expr: {
$and: [
{ $eq: ["$publication", "$$id"] },
{ "$eq": [{ "$year": "$subStartDate" }, new Date().getFullYear()] }
],
}
}
},
{ $project: { subStartDate: 1, subType: 1 } }
],
as: "founditem"
}
},
// {
// /* group by year and month of the subscription event */
// $group: {
// _id: { year: { $year: "$founditem.subStartDate" }, month: { $month: "$foundtitem.subStartDate" }, subType: "$founditem.subType" },
// count: { $sum: 1 }
// },
// },
// {
// /* sort descending (latest subscriptions first) */
// $sort: {
// '_id.year': -1,
// '_id.month': -1
// }
// },
], function (err, result) {
if (err) {
console.log(err);
} else {
res.json(result);
}
})
Which returns the desired data without the $group (commented out) but I need the $group to work or I'm going to have to map a dynamic array based on month and subtype which is completely inefficient.
When I'm diagnosing, it looks like this $group is the issue but I can't see how to fix as it works in the singular $year/$month group. So I tried the following:
{
/* group by year and month of the subscription event */
$group: {
_id: { year: { $year: "$subStartDate" }, month: { $month: "$subStartDate" }, subType: "$founditem.subType" },
count: { $sum: 1 }
},
},
And it returned the $founditem.subType fine, but any count or attempt to get $year or $month of the $founditem.subStartDate gave a BSON error.
The output from the single publication ID lookup in the reader collection call that works (and is plugging into the line graph perfectly) is:
[
{
"_id": {
"year": 2019,
"month": 11,
"subType": "digital"
},
"count": 1
},
{
"_id": {
"year": 2019,
"month": 11,
"subType": "print"
},
"count": 3
}
]
This is the output I'd like for ALL publications rather than just a single lookup of a publication ID within the reader collection.
Thank you for any assistance and please let me know if you need more details!!

Filter by joined sub-document

I am trying to filter a document by a sub-documents referred property. Assume that I have already created models for each schema. The simplified schemas are the following:
const store = new Schema({
name: { type: String }
})
const price = new Schema({
price: { type: Number },
store: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Store'
},
})
const product = new Schema({
name: {type: String},
prices: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Price'
}]
})
/*
Notation:
lowercase for schemas: product
uppercase for models: Product
*/
As a first approach I tried:
Product.find({'prices.store':storeId}).populate('prices')
but this does not work as filtering by a sub-document property is not supported on mongoose.
My current approach is using the aggregation framework. This is how the aggregation looks:
{
$unwind: '$prices'
},
{
$lookup: {
from: 'prices',
localField: 'prices',
foreignField: '_id',
as: 'prices'
}
},
{
$unwind: '$prices'
},
{
$lookup: {
from: 'stores',
localField: 'prices.store',
foreignField: '_id',
as: 'prices.store'
}
}, // populate
{
$match: {
'prices.store._id': new mongoose.Types.ObjectId(storeId)
}
}, // filter by store id
{ $group: { _id: '$id', doc: { $first: '$$ROOT' } } },
{ $replaceRoot: { newRoot: '$doc' } }
// Error occurs in $group & $replaceRoot
For example, before the last two stages if the record being saved is:
{
name: 'Milk',
prices: [
{store: 1, price: 3.2},
{store: 2, price: 4.0}
]
}
then the aggregation returned: (notice the product is the same but displaying each price in different results)
[
{
id: 4,
name: 'Milk',
prices: {
id: 10,
store: { _id: 1, name : 'Walmart' },
price: 3.2
}
},
{
id: 4,
name: 'Milk',
prices: {
id: 11,
store: { _id: 2, name : 'CVS' },
price: 4.0
},
}
]
To solve this issue I added the last part:
{ $group: { _id: '$id', doc: { $first: '$$ROOT' } } },
{ $replaceRoot: { newRoot: '$doc' } }
But this last part only returns the following:
{
id: 4,
name: 'Milk',
prices: {
id: 10,
store: { _id: 1, name : 'Walmart' },
price: 3.2
}
}
Now prices is an object, it should be an array and it should contain all prices (2 in this case).
Question
How to return all prices (as an array) with the store field populated and filtered by storeId?
Expected result:
{
id: 4,
name: 'Milk',
prices: [
{
id: 10,
store: { _id: 1, name : 'Walmart' },
price: 3.2
},
{
id: 11,
store: { _id: 2, name : 'CVS' },
price: 4.0
}]
}
EDIT
I want to filter products that contain prices in a given store. It should return the product with its prices, all of them.
I'm not totally convinced your existing pipeline is the most optimal, but without sample data to work from it's hard to really tell otherwise. So just working onward from what you have:
Using $unwind
var pipeline = [
// { $unwind: '$prices' }, // note: should not need this past MongoDB 3.0
{ $lookup: {
from: 'prices',
localField: 'prices',
foreignField: '_id',
as: 'prices'
}},
{ $unwind: '$prices' },
{ $lookup: {
from: 'stores',
localField: 'prices.store',
foreignField: '_id',
as: 'prices.store'
}},
// Changes from here
{ $unwind: '$prices.store' },
{ $match: {'prices.store._id': mongoose.Types.ObjectId(storeId) } },
{ $group: {
_id: '$_id',
name: { $first: '$name' },
prices: { $push: '$prices' }
}}
];
The points there start with:
Initial $unwind - Should not be required. Only in very early MongoDB 3.0 releases was this ever a requirement to $unwind an array of values before using $lookup on those values.
$unwind after $lookup - Is always required if you expect a "singular" object as matching, since $lookup always returns an array.
$match after $unwind - Is actually an "optimization" for pipeline processing and in fact a requirement in order to "filter". Without $unwind it's just a verification that "something is there" but items that did not match would not be removed.
$push in $group - This is the actual part the re-builds the "prices"array.
The key point you were basically missing was using $first for the "whole document" content. You really don't ever want that, and even if you want more than just "name" you always want to $push the "prices".
In fact you probably do want more fields than just name from the original document, but really you should therefore be using the following form instead.
Expressive $lookup
An alternate is available with most modern MongoDB releases since MongoDB 3.6, which frankly you should be using at minimum:
var pipeline = [
{ $lookup: {
from: 'prices',
let: { prices: '$prices' },
pipeline: [
{ $match: {
store: mongoose.Types.ObjectId(storeId),
$expr: { $in: [ '$_id', '$$prices' ] }
}},
{ $lookup: {
from: 'stores',
let: { store: '$store' },
pipeline: [
{ $match: { $expr: { $eq: [ '$_id', '$$store' ] } }
],
as: 'store'
}},
{ $unwind: '$store' }
],
as: 'prices'
}},
// remove results with no matching prices
{ $match: { 'prices.0': { $exists: true } } }
];
So the first thing to notice there is the "outer" pipeline is actually just a single $lookup stage, since all it really needs to do is "join" to the prices collection. From the perspective of joining to your original collection this is also true since the additional $lookup in the above example is actually related from prices to another collection.
This is then exactly what this new form does, so instead of using $unwind on the resulting array and then following on the join, only the matching items for "prices" are then "joined" to the "stores" collection, and before those are returned into the array. Of course since there is a "one to one" relationship with the "store", this will actually $unwind.
In short, the output of this simply has the original document with a "prices" array inside it. So there is no need to re-construct via $group and no confusion of what you use $first on and what you $push.
NOTE: I'm more than a little suspect of your "filter stores" statement and attempting to match the store field as presented in the "prices" collection. The question shows expected output from two different stores even though you specify an equality match.
If anything I suspect you might mean a "list of stores", which would instead be more like:
store: { $in: storeList.map(store => mongoose.Types.ObjectId(store)) }
Which is how you would work with a "list of strings" in both cases, using $in for matching against a "list" and the Array.map() to work with a supplied list and return each as ObjectId() values.
TIP: With mongoose you use a "model" rather than working with collection names, and the actual MongoDB collection names is typically the plural of the model name you registered.
So you don't have to "hardcode" the actual collection names for $lookup, simply use:
Model.collection.name
The .collection.name is an accessible property on all models, and can save you the trouble of remembering to actually name the collection for $lookup. It also protects you should you ever change your mongoose.model() instance registration in a way which alters the stored collection name with MongoDB.
Full Demonstration
The following is a self contained listing demonstrating both approaches as work and how they produce the same results:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/shopping';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const storeSchema = new Schema({
name: { type: String }
});
const priceSchema = new Schema({
price: { type: Number },
store: { type: Schema.Types.ObjectId, ref: 'Store' }
});
const productSchema = new Schema({
name: { type: String },
prices: [{ type: Schema.Types.ObjectId, ref: 'Price' }]
});
const Store = mongoose.model('Store', storeSchema);
const Price = mongoose.model('Price', priceSchema);
const Product = mongoose.model('Product', productSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k, m]) => m.deleteMany())
);
// Insert working data
let [StoreA, StoreB, StoreC] = await Store.insertMany(
["StoreA", "StoreB", "StoreC"].map(name => ({ name }))
);
let [PriceA, PriceB, PriceC, PriceD, PriceE, PriceF]
= await Price.insertMany(
[[StoreA,1],[StoreB,2],[StoreA,3],[StoreC,4],[StoreB,5],[StoreC,6]]
.map(([store, price]) => ({ price, store }))
);
let [Milk, Cheese, Bread] = await Product.insertMany(
[
{ name: 'Milk', prices: [PriceA, PriceB] },
{ name: 'Cheese', prices: [PriceC, PriceD] },
{ name: 'Bread', prices: [PriceE, PriceF] }
]
);
// Test 1
{
log("Single Store - expressive")
const pipeline = [
{ '$lookup': {
'from': Price.collection.name,
'let': { prices: '$prices' },
'pipeline': [
{ '$match': {
'store': ObjectId(StoreA._id), // demo - it's already an ObjectId
'$expr': { '$in': [ '$_id', '$$prices' ] }
}},
{ '$lookup': {
'from': Store.collection.name,
'let': { store: '$store' },
'pipeline': [
{ '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } }
],
'as': 'store'
}},
{ '$unwind': '$store' }
],
as: 'prices'
}},
{ '$match': { 'prices.0': { '$exists': true } } }
];
let result = await Product.aggregate(pipeline);
log(result);
}
// Test 2
{
log("Dual Store - expressive");
const pipeline = [
{ '$lookup': {
'from': Price.collection.name,
'let': { prices: '$prices' },
'pipeline': [
{ '$match': {
'store': { '$in': [StoreA._id, StoreB._id] },
'$expr': { '$in': [ '$_id', '$$prices' ] }
}},
{ '$lookup': {
'from': Store.collection.name,
'let': { store: '$store' },
'pipeline': [
{ '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } }
],
'as': 'store'
}},
{ '$unwind': '$store' }
],
as: 'prices'
}},
{ '$match': { 'prices.0': { '$exists': true } } }
];
let result = await Product.aggregate(pipeline);
log(result);
}
// Test 3
{
log("Single Store - legacy");
const pipeline = [
{ '$lookup': {
'from': Price.collection.name,
'localField': 'prices',
'foreignField': '_id',
'as': 'prices'
}},
{ '$unwind': '$prices' },
// Alternately $match can be done here
// { '$match': { 'prices.store': StoreA._id } },
{ '$lookup': {
'from': Store.collection.name,
'localField': 'prices.store',
'foreignField': '_id',
'as': 'prices.store'
}},
{ '$unwind': '$prices.store' },
{ '$match': { 'prices.store._id': StoreA._id } },
{ '$group': {
'_id': '$_id',
'name': { '$first': '$name' },
'prices': { '$push': '$prices' }
}}
];
let result = await Product.aggregate(pipeline);
log(result);
}
// Test 4
{
log("Dual Store - legacy");
const pipeline = [
{ '$lookup': {
'from': Price.collection.name,
'localField': 'prices',
'foreignField': '_id',
'as': 'prices'
}},
{ '$unwind': '$prices' },
// Alternately $match can be done here
{ '$match': { 'prices.store': { '$in': [StoreA._id, StoreB._id] } } },
{ '$lookup': {
'from': Store.collection.name,
'localField': 'prices.store',
'foreignField': '_id',
'as': 'prices.store'
}},
{ '$unwind': '$prices.store' },
//{ '$match': { 'prices.store._id': { '$in': [StoreA._id, StoreB._id] } } },
{ '$group': {
'_id': '$_id',
'name': { '$first': '$name' },
'prices': { '$push': '$prices' }
}}
];
let result = await Product.aggregate(pipeline);
log(result);
}
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
Which produces the output:
Mongoose: stores.deleteMany({}, {})
Mongoose: prices.deleteMany({}, {})
Mongoose: products.deleteMany({}, {})
Mongoose: stores.insertMany([ { _id: 5c7c79bcc78675135c09f54b, name: 'StoreA', __v: 0 }, { _id: 5c7c79bcc78675135c09f54c, name: 'StoreB', __v: 0 }, { _id: 5c7c79bcc78675135c09f54d, name: 'StoreC', __v: 0 } ], {})
Mongoose: prices.insertMany([ { _id: 5c7c79bcc78675135c09f54e, price: 1, store: 5c7c79bcc78675135c09f54b, __v: 0 }, { _id: 5c7c79bcc78675135c09f54f, price: 2, store: 5c7c79bcc78675135c09f54c, __v: 0 }, { _id: 5c7c79bcc78675135c09f550, price: 3, store: 5c7c79bcc78675135c09f54b, __v: 0 }, { _id: 5c7c79bcc78675135c09f551, price: 4, store: 5c7c79bcc78675135c09f54d, __v: 0 }, { _id: 5c7c79bcc78675135c09f552, price: 5, store: 5c7c79bcc78675135c09f54c, __v: 0 }, { _id: 5c7c79bcc78675135c09f553, price: 6, store: 5c7c79bcc78675135c09f54d, __v: 0 } ], {})
Mongoose: products.insertMany([ { prices: [ 5c7c79bcc78675135c09f54e, 5c7c79bcc78675135c09f54f ], _id: 5c7c79bcc78675135c09f554, name: 'Milk', __v: 0 }, { prices: [ 5c7c79bcc78675135c09f550, 5c7c79bcc78675135c09f551 ], _id: 5c7c79bcc78675135c09f555, name: 'Cheese', __v: 0 }, { prices: [ 5c7c79bcc78675135c09f552, 5c7c79bcc78675135c09f553 ], _id: 5c7c79bcc78675135c09f556, name: 'Bread', __v: 0 } ], {})
"Single Store - expressive"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', let: { prices: '$prices' }, pipeline: [ { '$match': { store: 5c7c79bcc78675135c09f54b, '$expr': { '$in': [ '$_id', '$$prices' ] } } }, { '$lookup': { from: 'stores', let: { store: '$store' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } } ], as: 'store' } }, { '$unwind': '$store' } ], as: 'prices' } }, { '$match': { 'prices.0': { '$exists': true } } } ], {})
[
{
"_id": "5c7c79bcc78675135c09f554",
"prices": [
{
"_id": "5c7c79bcc78675135c09f54e",
"price": 1,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
],
"name": "Milk",
"__v": 0
},
{
"_id": "5c7c79bcc78675135c09f555",
"prices": [
{
"_id": "5c7c79bcc78675135c09f550",
"price": 3,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
],
"name": "Cheese",
"__v": 0
}
]
"Dual Store - expressive"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', let: { prices: '$prices' }, pipeline: [ { '$match': { store: { '$in': [ 5c7c79bcc78675135c09f54b, 5c7c79bcc78675135c09f54c ] }, '$expr': { '$in': [ '$_id', '$$prices' ] } } }, { '$lookup': { from: 'stores', let: { store: '$store' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } } ], as: 'store' } }, { '$unwind': '$store' } ], as: 'prices' } }, { '$match': { 'prices.0': { '$exists': true } } } ], {})
[
{
"_id": "5c7c79bcc78675135c09f554",
"prices": [
{
"_id": "5c7c79bcc78675135c09f54e",
"price": 1,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
},
{
"_id": "5c7c79bcc78675135c09f54f",
"price": 2,
"store": {
"_id": "5c7c79bcc78675135c09f54c",
"name": "StoreB",
"__v": 0
},
"__v": 0
}
],
"name": "Milk",
"__v": 0
},
{
"_id": "5c7c79bcc78675135c09f555",
"prices": [
{
"_id": "5c7c79bcc78675135c09f550",
"price": 3,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
],
"name": "Cheese",
"__v": 0
},
{
"_id": "5c7c79bcc78675135c09f556",
"prices": [
{
"_id": "5c7c79bcc78675135c09f552",
"price": 5,
"store": {
"_id": "5c7c79bcc78675135c09f54c",
"name": "StoreB",
"__v": 0
},
"__v": 0
}
],
"name": "Bread",
"__v": 0
}
]
"Single Store - legacy"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', localField: 'prices', foreignField: '_id', as: 'prices' } }, { '$unwind': '$prices' }, { '$lookup': { from: 'stores', localField: 'prices.store', foreignField: '_id', as: 'prices.store' } }, { '$unwind': '$prices.store' }, { '$match': { 'prices.store._id': 5c7c79bcc78675135c09f54b } }, { '$group': { _id: '$_id', name: { '$first': '$name' }, prices: { '$push': '$prices' } } } ], {})
[
{
"_id": "5c7c79bcc78675135c09f555",
"name": "Cheese",
"prices": [
{
"_id": "5c7c79bcc78675135c09f550",
"price": 3,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
]
},
{
"_id": "5c7c79bcc78675135c09f554",
"name": "Milk",
"prices": [
{
"_id": "5c7c79bcc78675135c09f54e",
"price": 1,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
]
}
]
"Dual Store - legacy"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', localField: 'prices', foreignField: '_id', as: 'prices' } }, { '$unwind': '$prices' }, { '$match': { 'prices.store': { '$in': [ 5c7c79bcc78675135c09f54b, 5c7c79bcc78675135c09f54c ] } } }, { '$lookup': { from: 'stores', localField: 'prices.store', foreignField: '_id', as: 'prices.store' } }, { '$unwind': '$prices.store' }, { '$group': { _id: '$_id', name: { '$first': '$name' }, prices: { '$push': '$prices' } } } ], {})
[
{
"_id": "5c7c79bcc78675135c09f555",
"name": "Cheese",
"prices": [
{
"_id": "5c7c79bcc78675135c09f550",
"price": 3,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
}
]
},
{
"_id": "5c7c79bcc78675135c09f556",
"name": "Bread",
"prices": [
{
"_id": "5c7c79bcc78675135c09f552",
"price": 5,
"store": {
"_id": "5c7c79bcc78675135c09f54c",
"name": "StoreB",
"__v": 0
},
"__v": 0
}
]
},
{
"_id": "5c7c79bcc78675135c09f554",
"name": "Milk",
"prices": [
{
"_id": "5c7c79bcc78675135c09f54e",
"price": 1,
"store": {
"_id": "5c7c79bcc78675135c09f54b",
"name": "StoreA",
"__v": 0
},
"__v": 0
},
{
"_id": "5c7c79bcc78675135c09f54f",
"price": 2,
"store": {
"_id": "5c7c79bcc78675135c09f54c",
"name": "StoreB",
"__v": 0
},
"__v": 0
}
]
}
]

Resources