Populate field in an array of objects MongoDB and Mongoose - node.js

I'm building a comment system for my website. I've created this schema for the article and it's comments:
let articleSchema = mongoose.Schema({
language: {
type: String,
default: "english"
},
slug: {
type: String,
required: true
},
image: {
type: String,
required: true
},
title: {
type: String,
required: true
},
tags: {
type: [String],
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
date: {
type: Date,
default: Date.now()
},
description: {
type: String,
required: false
},
body: {
type: String,
required: true
},
translation: [
{
language: {
type: String,
required: true
},
title: {
type: String,
required: true
},
tags: {
type: [String],
required: true
},
description: {
type: String,
required: false
},
body: {
type: String,
required: true
}
}
],
comments: [{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
date: {
type: Date,
default: Date.now()
},
body: {
type: String,
required: true
}
}]
});
Now what I want to do is to populate every comment author field.
Let's say this is the article with a comment:
{
"comments": [
{
"author": "5f71d3b575f9f800943903af",
"date": "some date here",
"body": "comment body"
}
]
}
Now what I want to get after I populated all of this is this:
{
"comments": [
{
"author": {
"username": "Username",
"avatar": "fox"
},
"date": "some date here",
"body": "comment body"
}
]
}
How can I do that?
Here's my current code:
router.get('/article/:id', async (req, res) => {
const { id: articleId } = req.params;
const lang = req.i18n.language;
const langName = new Intl.DisplayNames(['en'], { type: "language" }).of(lang).toLowerCase();
try {
console.log(langName);
if (req.i18n.language === "en") {
var article = await Article.findById(articleId)
.populate([
{path:'comments.author', select:'+photo +username'},
{path:'author', select:'+photo +username'}
])
.select("title image tags author date description body comments");
} else {
var article = await Article
.aggregate([
{
$match: {
"_id": ObjectId(articleId)
}
}, {
$unwind: "$translation"
}, {
$match: {
"translation.language": langName
}
}, {
$group: {
_id: "$_id",
image: { $first: "$image" },
title: { $first: "$translation.title" },
tags: { $first: "$translation.tags" },
author: { $first: "$author" },
date: { $first: "$date" },
description: { $first: "$translation.description" },
body: { $first: "$translation.body" },
comments: { $first: "$comments" }
}
},
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author"
}
},
{
$unwind: {
path: "$author"
}
},
{ ///////////////////// Here I need to populate the comment author. How can I do it?
$lookup: {
from: "users",
localField: "comments.author",
foreignField: "_id",
as: "comments.author"
}
},
{
$unwind: {
path: "$comments.author"
}
},
{
$project: {
image: 1,
title: 1,
tags: 1,
date: 1,
description: 1,
body: 1,
// comments: 1,
"comments.date": 1,
"comments.body": 1,
"comments.author.username": 1,
"comments.author.avatar": 1,
"author.username": 1,
"author.avatar": 1
}
}
], function(err, result) {
console.log(err)
return result;
});
console.log(article[0].comments[0]);
console.log(article);
}
if (!article) throw new Error();
} catch {
return res.status(404).render('errors/404');
}
res.render('articles/article', {
article: article[0]
});
});

So after some digging I came up with this solution:
router.get('/article/:id', async (req, res) => {
const { id: articleId } = req.params;
const lang = req.i18n.language;
const langName = new Intl.DisplayNames(['en'], { type: "language" }).of(lang).toLowerCase();
try {
if (req.i18n.language === "en") {
var article = await Article.findById(articleId)
.populate([
{path:'comments.author', select:'+photo +username'},
{path:'author', select:'+photo +username'}
])
.select("title image tags author date description body comments");
} else {
var article = await Article
.aggregate([
{
$match: {
"_id": ObjectId(articleId)
}
}, {
$unwind: "$translation"
}, {
$match: {
"translation.language": langName
}
},
{
$lookup: {
from: "users",
localField: "author",
foreignField: "_id",
as: "author"
}
},
{
$unwind: {
path: "$author"
}
},
{
$unwind: {
path: "$comments"
}
},
{
$lookup: {
from: "users",
localField: "comments.author",
foreignField: "_id",
as: "comments.author"
}
},
{
$group: {
_id: "$_id",
root: { $mergeObjects: '$$ROOT' },
image: { $first: "$image" },
title: { $first: "$translation.title" },
tags: { $first: "$translation.tags" },
author: { $first: "$author" },
date: { $first: "$date" },
description: { $first: "$translation.description" },
body: { $first: "$translation.body" },
comments: { $push: "$comments" }
}
},
{
$project: {
image: 1,
title: 1,
tags: 1,
date: 1,
description: 1,
body: 1,
"comments.date": 1,
"comments.body": 1,
"comments.author.username": 1,
"comments.author.avatar": 1,
"author.username": 1,
"author.avatar": 1
}
}
]);
}
if (!article) throw new Error();
} catch {
return res.status(404).render('errors/404');
}
res.render('articles/article', {
article: article[0]
});
});

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,
},
});

Perform $lookup with object value in array from pipeline

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

Mongoose $lookup object returns empty array

I have tried other similar kind of questions available but nothing seems to work for me.
I have two collections:
leads:
const mongoose = require("mongoose");
const id = mongoose.Schema.Types.ObjectId;
const leadsSchema = mongoose.Schema(
{
_id: id,
userId: { type: id, ref: "User", required: true },
leadName: String,
leads: [
{
_id: id,
name: String,
status: { type: String, required: false, default: "New" },
leadActivity: { type: String, required: false, default: "No Campaign Set" },
headline: { type: String, required: false },
location: { type: String, required: false },
leadType: { type: id, ref: "LeadsCategory", required: true },
}
],
campaignAssociated: {type: id, ref: "campaign"},
},
{
timestamps: true
}
);
module.exports = mongoose.model("lead", leadsSchema);
leadCategory
const mongoose = require("mongoose");
const leadsCategorySchema = mongoose.Schema(
{
_id: mongoose.Schema.Types.ObjectId,
name: {
type: String,
required: false,
},
leadsData: [{ type: Array, ref: "lead" }],
},
{ timestamps: true }
);
module.exports = mongoose.model("LeadsCategory", leadsCategorySchema);
I am trying to reference/populate the name of the lead from leadscategory schema into the leads
exports.get_single_lead_info = (req, res) => {
const { userId } = req.user;
const { leadid } = req.body;
let idToSearch = mongoose.Types.ObjectId(leadid);
Lead.aggregate([
{
$lookup: {from: 'leadscategories', localField: 'leadType', foreignField: 'name', as: 'type as'}
},
{
$match: {
userId: mongoose.Types.ObjectId(userId),
},
},
{
$unwind: "$leads",
},
{
$match: {
"leads._id": idToSearch,
},
},
])
.exec(function (err, result) {
if (err) {
return res.status(400).json({ message: "Unable to fetch data", err });
}
if (!result.length) {
res.status(404).json("No result found");
} else {
res.status(200).json({ message: "Lead info found", result });
}
});
};
But it outputs me the lookup result as an empty array everytime:
{
"message": "Lead info found",
"result": [
{
"_id": "5ece11cbac50c434dc4b7f2c",
"leadName": "python",
"leads": {
"status": "New",
"leadActivity": "Campaign Set",
"name": "Hailey",
"headline": "Machine Learning | Python",
"location": "New Delhi Area, India",
"_id": "5ece11cbac50c434dc4b7f29",
"leadType": "5ebce0f81947df2fd4eb1060"
},
"userId": "5eba83d37d4f5533581a7d58",
"createdAt": "2020-05-27T07:07:55.231Z",
"updatedAt": "2020-05-27T10:47:42.098Z",
"__v": 0,
"type as": [] //<--- Need lead type name associated inside this
}
]
}
Input: "leadid": "5ece11cbac50c434dc4b7f29"
Any help appreciated.
[
{
$match: {
userId: mongoose.Types.ObjectId(userId),
},
},
{
$unwind: "$leads",
},
{
$match: {
'leads._id': idToSearch,
},
},
{
$lookup: {
from: 'leadscategories',
localField: 'leads.leadType',
foreignField: '_id',
as: 'type as'
}
},
]

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?

Get conversation's last message with unread count

I'm making an in app messaging system in which I have to show the list of conversations with their last message and the unread count. My schema is as follows--
var schema = new Schema({
senderID: {
type: Schema.Types.ObjectId,
ref: 'Member'
},
receiversID: [{
type: Schema.Types.ObjectId,
ref: 'Member'
}],
content: {
type: String,
default: ''
},
isRead: {
type: Boolean,
default: false,
},
createdAt: {
type: Number,
default: Date.now
}
});
I did this initially to get all the conversations with their last message --
messageModel.aggregate(
[{ $match: { senderID: userId } },
{ $unwind: '$receiversID' },
{ $sort: { createdAt: -1 } },
{ $group: { _id: '$receiversID', unreadCount: { $sum: { $cond: [{ $eq: ["$isRead", false] }, 1, 0] } }, senderID: { $first: '$senderID' }, receiversID: { $first: '$receiversID' }, content: { $first: '$content' } } },
{ $skip: pagingData.pageSize * (pagingData.pageIndex - 1) },
{ $limit: pagingData.pageSize }
], function (err, docs) {
resolve(docs);
}
);
But it doesn't shows the messages if you are a receiver. I want to show the conversation whether you are receiver or sender.
i use something like this:
{
'$or': [
{
'$and': [
{
'receiversID': userId
}, {
'senderID': toUserId
}
]
}, {
'$and': [
{
'receiversID': toUserId
}, {
'senderID': userId
}
]
},
],
}

Resources