Perform $lookup with object value in array from pipeline - node.js

So I have 3 models user, property, and testimonials.
Testimonials have a propertyId, message & userId. I've been able to get all the testimonials for each property with a pipeline.
Property.aggregate([
{ $match: { _id: ObjectId(propertyId) } },
{
$lookup: {
from: 'propertytestimonials',
let: { propPropertyId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ['$propertyId', '$$propPropertyId'] }],
},
},
},
],
as: 'testimonials',
},
},
])
The returned property looks like this
{
.... other property info,
testimonials: [
{
_id: '6124bbd2f8eacfa2ca662f35',
userId: '6124bbd2f8eacfa2ca662f29',
message: 'Amazing property',
propertyId: '6124bbd2f8eacfa2ca662f2f',
},
{
_id: '6124bbd2f8eacfa2ca662f35',
userId: '6124bbd2f8eacfa2ca662f34',
message: 'Worth the price',
propertyId: '6124bbd2f8eacfa2ca662f2f',
},
]
}
User schema
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
Property schema
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
location: {
type: String,
required: true,
},
Testimonial schema
propertyId: {
type: ObjectId,
required: true,
},
userId: {
type: ObjectId,
required: true,
},
testimonial: {
type: String,
required: true,
},
Now the question is how do I $lookup the userId from each testimonial so as to show the user's info and not just the id in each testimonial?
I want my response structured like this
{
_id: '6124bbd2f8eacfa2ca662f34',
name: 'Maisonette',
price: 100000,
testimonials: [
{
_id: '6124bbd2f8eacfa2ca662f35',
userId: '6124bbd2f8eacfa2ca662f29',
testimonial: 'Amazing property',
propertyId: '6124bbd2f8eacfa2ca662f34',
user: {
_id: '6124bbd2f8eacfa2ca662f29',
firstName: 'John',
lastName: 'Doe',
email: 'jd#mail.com',
}
},
{
_id: '6124bbd2f8eacfa2ca662f35',
userId: '6124bbd2f8eacfa2ca662f27',
testimonial: 'Worth the price',
propertyId: '6124bbd2f8eacfa2ca662f34',
user: {
_id: '6124bbd2f8eacfa2ca662f27',
firstName: 'Sam',
lastName: 'Ben',
email: 'sb#mail.com',
}
}
]
}

You can put $lookup stage inside pipeline,
$lookup with users collection
$addFields, $arrayElemAt to get first element from above user lookup result
Property.aggregate([
{ $match: { _id: ObjectId(propertyId) } },
{
$lookup: {
from: "propertytestimonials",
let: { propPropertyId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$propertyId", "$$propPropertyId"] }
}
},
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}
},
{
$addFields: {
user: { $arrayElemAt: ["$user", 0] }
}
}
],
as: "testimonials"
}
}
])
Playground

Related

How to aggregate with many conditions on MongoDB. Double $lookup etc

How to display "hardest category" based on in which "study" size of notLearnedWords was the highest. MongoDB Aggregation
I have these 3 models:
Study
WordSet
Category
Study model has reference into WordSet, then WordSet has reference into Category.
And based on Studies i'm displaying statistics.
How i can display "The hardest category" based on size of "notLearnedWords" was the highest?
I don't know on which place i should start with that querying.
For now i display "hardestCategory" as element that is most used.
I think that condition would look something like this:
{ $max: { $size: '$notLearnedWords' } } // size of the study with most notLearnedWords
I would achieve a response like this:
"stats": [
{
"_id": null,
"numberOfStudies": 4,
"averageStudyTime": 82.5,
"allStudyTime": 330,
"longestStudy": 120,
"allLearnedWords": 8
"hardestCategory": "Work" // only this field is missing
}
]
I've tried to do it like this:
const stats = await Study.aggregate([
{ $match: { user: new ObjectID(currentUserId) } },
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'currentUser',
},
},
{
$lookup: {
from: 'wordsets',
let: { wordSetId: '$learnedWordSet' },
pipeline: [
{ $match: { $expr: { $eq: ['$_id', '$$wordSetId'] } } },
{
$project: {
_id: 0,
category: 1,
},
},
{ $unwind: '$category' },
{
$group: {
_id: '$category',
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: 1 },
{
$lookup: {
from: 'categories',
localField: '_id',
foreignField: '_id',
as: 'category',
},
},
{
$project: {
_id: 0,
category: { $arrayElemAt: ['$category.name', 0] },
},
},
],
as: 'wordSet',
},
},
{
$group: {
_id: null,
numberOfStudies: { $sum: 1 },
averageStudyTime: { $avg: '$studyTime' },
allStudyTime: { $sum: '$studyTime' },
longestStudy: { $max: '$studyTime' },
allLearnedWords: {
$sum: { $size: '$learnedWords' },
},
hardestCategory: {
$first: {
$first: '$wordSet.category',
},
},
studyWithMostNotLearnedWords: { $max: { $size: '$notLearnedWords' } },
},
},
]);
Study
const studySchema = new mongoose.Schema({
name: {
type: String,
},
studyTime: {
type: Number,
},
learnedWords: [String],
notLearnedWords: [String],
learnedWordSet: {
type: mongoose.Schema.Types.ObjectId,
ref: 'WordSet',
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
});
WordSet
const wordSetSchema = new mongoose.Schema({
name: {
type: String,
},
category: {
type: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true,
},
],
},
});
Category
const categorySchema = new mongoose.Schema({
name: {
type: String,
},
});

Use group by on a group by result in mongoose

I am working on a NodeJS and a Mongoose Project and I have the following two schemas.
UserSchema.js
const UserSchema = mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
},
incharge: {
type: String,
enum: ['Adhihariharan', 'Anuja', 'Dhivya', 'Govind', 'Joann'],
required: true
},
)}
ContactSchema.js
const ContactSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: [true, 'Please add a name'],
},
status: {
type: String,
enum: [
'Not Called',
'Wrong Number',
'Called/Declined',
'Called/Not Reachable',
'Called/Postponed',
'Called/Accepted',
'Emailed/Awaiting Response',
'Emailed/Declined',
'Emailed/Confirmed',
],
default: 'Not Called',
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true,
},
I am looking for a query which would give me a result which looks as the following:
[
{
_id: "5d7a514b5d2c12c7449be048",
name: "Benita",
incharge: "Joann",
statuses: [
{ status: "Not Called", count: 1 },
{ status: "Called/Accepted", count: 1 },
{ status: "Called/Declined", count: 1 },
{ status: "Called/Not Reachable", count: 1 },
]
},
{
_id: "5d7a514b5d2c12c7449be048",
name: "Febia",
incharge: "Dhivya",
statuses: [
{ "Not Called": 2 },
{ "Called/Postponed": 2 },
{ "Called/Declined": 3 },
{ "Called/Not Reachable": 1 },
]
},
... and so on
]
Here, the integer, is the number of times that status appears for a particular user and in charge is the manager in charge of the user. The _id mentioned is the ID of the user.
The _id, user, in charge belong to the UserSchema and the status belongs to the ContactSchema
I have tried the following query:
teams = await Contact.aggregate([
{
$group: {
_id: { user: '$user', status: '$status' },
count: { $sum: '$count' },
},
},
{
$lookup: {
from: 'members',
localField: '_id.user',
foreignField: '_id',
as: 'user',
},
},
{
$unwind: { path: '$user' },
},
{
$project: {
'user.name': 1,
'user.incharge': 1,
count: 1,
},
},
{
$sort: { 'user.incharge': 1, 'user.name': 1 },
},
]);
And the following was the output:
{
_id: { user: 5ff52b10fa237b001c93ef18, status: 'Not Called' },
count: 1,
user: { name: 'Benita', incharge: 'Joann' }
},
{
_id: { user: 5ff4ca05fa237b001c93ef15, status: 'Not Called' },
count: 2,
user: { name: 'Febia', incharge: 'Dhivya' }
},
{
_id: { user: 5ff4ca05fa237b001c93ef15, status: 'Called/Accepted' },
count: 4,
user: { name: 'Febia', incharge: 'Dhivya' }
}
Can someone please help me get the desired result?
Thanks in advance.
EDIT:
I did try #turivishal's approach but this is what I got:-
{
_id: 5ff52b10fa237b001c93ef18,
name: 'Sadana',
incharge: 'Joann',
statuses: [ [Object] ]
},
{
_id: 5ff4ca05fa237b001c93ef15,
name: 'Sudarshan B',
incharge: 'Joann',
statuses: [ [Object], [Object] ]
}
Can you please tell me how I can access the [Object] inside the status array in mongoose so that I can get a result as below...
{
_id: "5ff4ca05fa237b001c93ef15",
name: "Sudarshan B",
incharge: "Joann",
statuses: [
{ "Not Called": 2 },
{ "Called/Postponed": 2 },
]
},
You can try lookup with aggregation pipeline,
$lookup with contact pass _id in let,
$match user id condition
$group by status and get total count
$project to change name of the key and value
$addFields to convert statuses array to object using $arrayToObject
teams = await User.aggregate([
{
$lookup: {
from: "contact",
let: { user: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$$user", "$user"] } } },
{
$group: {
_id: "$status",
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
k: "$_id",
v: "$count"
}
}
],
as: "statuses"
}
},
{ $addFields: { statuses: { $arrayToObject: "$statuses" } } }
])
Playground

Mongodb $lookup/populate and summarize

I'm trying to join two schema and summarize the total price.
This is the schema:
const Product = new mongoose.Schema({
name: { type: String, required: true },
price: Number
})
const Order = new mongoose.Schema({
fullname: { type: String, required: true },
address: { type: String, required: true },
products: [
{
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
},
quantity: Number,
},
],
})
I want to create aggregation to get orders with total price.so it could be like:
[
{
fullname: 'jhon doe',
address: 'NY 1030',
products: [
{
product: {
name: 'Piano',
price: 50
},
quantity: 10,
},
],
price: 500
}
]
I try to use aggregation framework without any success, any idea?
Updated
As the question needs the price to be calculated by sum of the multiplication of quantity and product price, It can be done with below code:
db.getCollection('orders').aggregate([
{ $unwind: { path: '$products' } },
{
$lookup: {
from: 'products',
localField: 'products.product',
foreignField: '_id',
as: 'p',
},
},
{ $unwind: { path: '$p' } },
{
$group: {
_id: '$_id',
price: { $sum: { $multiply: ['$p.price', '$products.quantity'] } },
fullname: { $first: '$fullname' },
address: { $first: '$address' },
products: { $push: { product: '$p', quantity: '$products.quantity' } },
},
},
])
----------------------------------------------------------------------------------------------------------------------------
You can use $lookup in aggregation as below:
db.getCollection('orders').aggregate([
{
$lookup: {
from: 'products',
localField: 'products.product',
foreignField: '_id',
as: 'p',
},
},
{ $unwind: { path: '$p' } },
{
$project: {
fullname: 1,
address: 1,
products: {
product: '$p',
quantity: 1,
},
price: 1,
},
},
])

Implementing joins on multiple collections

I am trying to create a social networking application which can have connect (followers, following), posts, comments, likes, shares, etc. This is a MVP project, but still i wanted to explore mongoDB for this use case. I am having some doubt regarding the performance of this application.
I have three collection:
Posts: This is where a new post shall be added. This collection contains all the details related to a post.
Schema:
const postSchema = new mongoose.Schema({
user_id: String,
title: String,
text: String,
type: { type: String, enum: ["music", "movie", "tv"] },
mediaUrl: String,
thumbnailUrl: String,
accessType: { type: Number, enum: [1, 2, 3] },
tags: [String],
like_count: Number,
comment_count: Number,
share_count: Number,
insertDate: {
type: Date,
default: () => {
return new Date();
}
}
});
Feeds: This collection just add a metadata of user and post including tags. This i intend to use to get the relevant feed for a particular user.
Schema:
const feedSchema = new mongoose.Schema({
post_id: String,
user_id: String,
isTag: Boolean,
isPublic: Boolean,
insertDate: {
type: Date,
default: () => {
return new Date();
}
},
modifiedDate: {
type: Date,
default: () => {
return new Date();
}
}
});
Connects: This collection is for the relationship of users.
Schema:
const connectSchema = new mongoose.Schema({
followed_by: String,
user_id: String,
insertDate: {
type: Date,
default: () => {
return new Date();
}
}
});
My approach was to first find the posts from feeds collection basis users whom I am following, then fetching the posts from post collection.
Here goes my attempted query:
db.connects.aggregate([
{ $match: { followed_by: "5cbefd61d3b53a4aaa9a2b16" } },
{
$lookup: {
from: "feeds",
let: { user_id: "$user_id" },
pipeline: [{ $match: { $expr: { $or: [{ $eq: ["$user_id", "$$user_id"] }, { isPublic: true }] } } }],
as: "feedData"
}
},
{ $unwind: "$feedData" },
{ $replaceRoot: { newRoot: "$feedData" } },
{ $group: { _id: { post_id: { $toObjectId: "$post_id" }, modifiedDate: { $toLong: "$modifiedDate" } } } },
{ $replaceRoot: { newRoot: "$_id" } },
{ $sort: { modifiedDate: -1 } },
{ $skip: 0 },
{ $limit: 10 },
{
$lookup: { from: "posts", localField: "post_id", foreignField: "_id", as: "postData" }
},
{ $unwind: "$postData" },
{ $replaceRoot: { newRoot: "$postData" } },
{ $addFields: { userId: { $toObjectId: "$user_id" } } },
{
$lookup: { from: "users", localField: "userId", foreignField: "_id", as: "userData" }
},
{
$project: {
post_id: "$_id",
user_id: 1,
title: 1,
text: 1,
typeaccessType: 1,
mediaUrl: 1,
thumbnailUrl: 1,
insertDate: { $toLong: "$insertDate" },
like_count: 1,
comment_count: 1,
share_count: 1,
user_email: { $arrayElemAt: ["$userData.email", 0] },
user_profile_pic: { $arrayElemAt: ["$userData.profile_pic", 0] },
username: { $arrayElemAt: ["$userData.firstname", 0] }
}
}
]).pretty();
Please share your feedback on:
Which index should I use to boost up the performance?
Is there is a better way of doing the same in mongoDB. Also if any part of the query can be optimised?

how to get count of unread messages against each user Mongoose

I want to select user list from users collection and if a user has an unread message which status is 0/false in messages schema then select unread messages count too, which can be 0/5/10/1/0. thanking you in advance! i am sure it can be done with aggregate framework and already tried some query(below) but with that i am not getting what i need to be the result.
//User Schema
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
email:{type: String, required: true, unique: true, minlength: 3, trim: true},
password: { type: String, required: true, minlength: 6, trim: true },
});
export default mongoose.model('user', userSchema);
//Message Schema
import mongoose from 'mongoose';
const messageSchema = new mongoose.Schema({
from: { type: mongoose.Schema.Types.ObjectId, ref: 'user' },
to: { type: mongoose.Schema.Types.ObjectId, ref: 'user' },
text: { type: String, trim: true },
unread: { type: Boolean, default: false }
});
messageSchema.set('timestamps', true);
export default mongoose.model('message', messageSchema);
I want the result should be
[ { _id: 5cc984981fa38539f4a61ce0,
name: 'Jhon Doe',
unreadTotal: 5 },
{ _id: 5cc98f651fa38539f4a61cfd,
name: 'Markas',
unreadTotal: 3 },
{ _id: 5cc994b164745026c4e25546,
name: 'Mike',
unreadTotal: 0 } ]
// i tried the following but getting unread of total not for each user
const userId = 'loggedinUserId'
const userList = await User.aggregate([
{
$match: { _id: {$ne: ObjectId(userId) } }
},
{ $lookup:
{
from: 'messages',
let: { 'to': ObjectId(userId) },
pipeline: [
{
$match:
{
'unread': false,
$expr: { $eq: [ '$$to', '$to' ] }
}
},
{ $count: 'count' }
],
as: 'messages'
}
},
{
$addFields:
{
'unreadTotal': { $sum: '$messages.count' }
}
}
]);
Not sure this will help you or not, but you will get all unread messages of each user against the logged in user as you have described and later you can do whatever you want with like count etc.
const userList = await User.aggregate([
{
$match: { _id: {$ne: ObjectId(userId) } }
},
{
$lookup:
{
from: 'messages',
localField: '_id',
foreignField: 'from',
as: 'messages'
},
},
{
$project:
{
name: 1,
email: 1,
profilePhoto: 1,
messages:
{
$filter:
{
input: "$messages",
as: "message",
cond: {
$and: [
{ $eq: [ '$$message.unread', false ] },
{ $eq: [ '$$message.to', ObjectId(userId) ] }
]
}
}
}
}
}
]);

Resources