mongoose aggregation not returning expected result - node.js

I am currently learning aggregation and some related methods, after following mongoose and mongodb docs i tried it. but i am having problem.
Follow.aggregate([
{ $match: { user: mongoose.Types.ObjectId(userId) } },
{ $unwind: '$followers' },
{
$lookup: {
from: 'accounts',
localField: 'followers',
foreignField: '_id',
as: 'followers'
}
},
{ $project: { name: 1, photo: 1 } },
]).exec((err, followers) => {
if (err) throw err;
console.log(followers);
res.send(followers);
});
I want to get the followers of that userID and select the followers names and photo, but i am only getting the objectid of the matched document
[ { _id: 5bfe2c529419a560fb3e92eb } ]
expected output
[ { _id: 5bfe2c529419a560fb3e92eb , name: 'john doe", photo: 'cat.jpg'} ]
Follow Model
const FollowSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
},
following: [{
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
}],
followers: [{
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
}],
});

When the $project stage is wrong you would get that result. Try removing it first and see the output. To me it seems you should have something like this there:
{ $project: { "followers.name": 1, "followers.photo": 1 } },

Related

How to find sum in Mongo aggregate method with match in different collection in an array of object?

I am trying to find the sum by matching array of object using mongoose. I have 2 collection such as
const accountSchema = new mongoose.Schema({
groupId: {
type: Number,
required: true
},
account_no: {
type: String,
required: true
},
account_name: {
type: String,
required: true
},
opening_balance: {
type: Number,
default: 0
}
})
And second collection as:
const mongoose = require('mongoose')
const AutoIncrement = require('mongoose-sequence')(mongoose);
const accountJournalSchema = new mongoose.Schema({
journal_no: {
type: Number
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'Users',
required: [true, 'User ID is required.'],
},
groupId: {
type: Number,
required: true
},
date: {
type: Date,
required: true
},
receipt: [
{
account_no: {
type: mongoose.Schema.ObjectId,
ref: 'Accounts',
required: true
},
debit: {
type: Number,
default: 0
},
credit: {
type: Number,
default: 0
},
}
]
})
And my aggregate method is:
await Accounts.aggregate([
{
$match: {
$and: [
{ groupId: {$eq: parseInt(req.params.group_id)} },
{ 'Account_jour.groupId': { $eq: parseInt(req.params.group_id) } }
]
}
},
{ unwind: '$Account_jour' },
{
$lookup: {
from : 'account_journals',
localField: '_id',
foreignField: 'receipt.account_no',
as: 'Account_jour'
}
}
])
I am getting error from the above statement:
Arguments must be aggregate pipeline operators
And after solving the issue I also want to find the sum of debit and credit.
Thank you!!
Try this:
await Accounts.aggregate([
{
$match: {
$and: [{ groupId: { $eq: parseInt(req.params.group_id) } }, { "Account_jour.groupId": { $eq: parseInt(req.params.group_id) } }]
}
},
{ $unwind: '$Account_jour' },
{
$lookup: {
from : 'account_journals',
localField: '_id',
foreignField: 'receipt.account_no',
as: 'Account_jour'
}
}
])

how to sort after lookup and project aggregation in mongodb?

I have 2 Collections one for users and other for posts(Posts colllection have _id of users as postedBy).
In users collection each user is having friends array which have _id of users in it.I want to get all the Posts of My friends and mine post in sorted order(sorted By CreatedAt).
This is my Userschema in which i am having friends array of mongoose object type ref to user collection,
here i'm storing users id who is friend.
`//UserSchema
const userSchema = new Schema({
profileImg : {
type: String,
},
name: {
type: String,
required: [true, 'Please Enter Your Name!']
},
about: {
type: String,
},
email: {
type: String,
required: [true, 'Please Enter Email!'],
unique: [true, 'Already Registered!'],
match: [/\S+#\S+\.\S+/, 'is invalid!']
},
password: {
type: String,
required: [true, 'Please Enter Your Password!'],
},
friends: [{
type: mongoose.Types.ObjectId,
ref: 'USER'
}],
address: {
line1: {
type: String,
required: [true, 'Please Enter Your Address!']
},
line2: {
type: String
},
city: {
type: String,
required: [true, 'Please Enter Your City!']
},
state: {
type: String,
required: [true, 'Please Enter Your State!']
},
}
}, { timestamps: true })
This is my Post Schema where userId is ref to users collection and here the _id of user who is uploading post is saved.
//POST SCHEMA
const postSchema = new Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "USER",
required: true
},
postImage: {
type: String,
required: [true, 'Please Upload the Image!']
},
caption: {
type: String
},
likes: [likeSchema],
comments: [commentSchema]
}, { timestamps: true })
`
What I am Doing:
1st I am finding the user through _id
2nd from found user's friend array ,lookup in posts collection to get post of friends
3rd Now to get owns post again look up in post collection with own _id
4th concat the both array obtain from friend post and user post as Posts
Now here after step 4 i want to sort the Posts by createdAt but its not working..
How to sort it?
const posts = await User.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(req.user_id)
}
},
{
$lookup: {
from: "posts",
localField: "friends",
foreignField: "userId",
as: "friendposts"
}
},
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "userposts"
}
},
{
$project: {
"Posts": {
$concatArrays: ["$friendposts", "$userposts"]
},
_id: 0
}
}
])
you can use 1 lookup instead of 2 .
for sorting you have 3 ways
sort in the code level (using sort function)
use $unwind $sort and group (if mongo db version is less than 5.2)
use $sortArray (applicable for mongodb version 5.2+)
if using 2nd method.
User.aggregate([
{
'$match': {
'_id': mongoose.Types.ObjectId(req.user_id)
}
}, {
'$addFields': {
'users': {
'$concatArrays': [
'$friends', [
mongoose.Types.ObjectId(req.user_id)
]
]
}
}
}, {
'$lookup': {
'from': 'posts',
'localField': 'users',
'foreignField': 'userId',
'as': 'posts'
}
}, {
'$unwind': {
'path': '$posts'
}
}, {
'$sort': {
'posts.createdAt': -1
}
}, {
'$group': {
'_id': '$_id',
'posts': {
'$push': '$posts'
},
'name': {
'$first': '$name'
}
}
}
])
you can add any other field needed in final response like wise i added name .

use mongoose.populate along with mongoose-pagivate-v2, when data is inside array of object

I am getting some data in an array of object like this :
{
"success": true,
"result": {
"docs": [
{
"_id": "60a602901a74f62935a4898f",
"user": "607030ba3c82e235443db610",
"weekNum": 19,
"__v": 0,
"createdAt": "2021-05-20T06:32:48.742Z",
"data": [
{
"activity": "6063f898232d3f2acca5d2ae",
"_id": "60a6063668f27715b0f08753",
"project": "60702d1f3c82e235443db5ff",
"task": "60702d3d3c82e235443db601",
"workingDate": "2021-05-10T18:30:00.000Z",
"dayVal": 1,
"description": ""
}
],
"managersComment": "leleleleelelel",
"status": "Submitted",
"updatedAt": "2021-05-20T06:48:22.163Z"
}
],
"paginator": {
"itemCount": 1,
"offset": 0,
"perPage": 10000,
"pageCount": 1,
"currentPage": 1,
"slNo": 1,
"hasPrevPage": false,
"hasNextPage": false,
"prev": null,
"next": null
}
}
}
my schema for this collection in like this:
const timesheetSchema = new Schema({
managersComment: {
type: String
},
weekNum: {
type: Number
},
data:[{
project: {
type: Schema.ObjectId,
ref: projectModel
},
task: {
type: Schema.ObjectId,
ref: taskModel
},
activity: {
type: Schema.ObjectId,
default: null,
ref: activityModel
},
workingDate: {
type: Date
},
dayVal: {
type: Number
},
description: {
type: String
},
}],
user: { type: ObjectId, ref: userModel },
status: {
type: String,
enum: ['Saved', 'Submitted', 'Approved', 'Rejected', 'Reset']
},
}, { timestamps: true });
timesheetSchema.plugin(mongoosePaginate);
const timesheetModel = mongoose.model('timesheet', timesheetSchema);
my code for getting data is something like this:
try {
console.log('populateRequired --------------------------------------------------')
const populateArray = [
{ path: "task", select: "taskName" },
{ path: "project", select: "projectName" },
{ path: "activity", select: "title" },
];
const query = {
user: req.params.userId,
status: req.query.status,
};
const paginationParams = {
populate: populateArray,
customLabels: customLabels,
limit: req.query.limit,
};
console.log("USER QUERY ", query);
const userTimesheet = await getTimesheetDataByUserId(
query,
paginationParams
);
console.log(userTimesheet);
res.send({ success: true, result: userTimesheet });
} catch (err) {
console.log(err);
next(err);
}
But as shown in return data above i am not getting populate applied in data array. Please help not sure what to do.
According to the data you posted, I think that the issue is that you're not creating virtual fields to populate with your references. Your fields project, task and activity in each array element, or user, are meant to be ids referring to the corresponding models. But those ids alone will not implement the population, they are only the pointers that the population will need in order to be executed. To make that a little bit more clear, I would change those names to userId: { type: ObjectId, ref: userModel }.
After that, you will need to create the virtual fields:
timesheetSchema.virtual("user", {
ref: "userModel",
localField: "userId",
foreignField: "_id",
justOne: true,
});
Finally, if you want to have the virtual field timesheet.user populated each time you query your collection, you will have to add some middleware to your schema. For me, the most reasonable way to make this work is:
timesheetSchema.pre("find", function (next) {
this.populate("user");
next();
});
Just to have a complete solution: I think this will solve your problem for the timesheet.user field. But I don't think it will work in your data array. In fact, I'm not 100% sure the way you're defining it is really going to work: creating a timesheet with an array of imputations doesn't make too much sense to me. A more coherent approach would be creating a collection of all the imputations that looked like this:
const dataSchema = new Schema({
projectId: {
type: Schema.ObjectId,
ref: projectModel
},
taskId: {
type: Schema.ObjectId,
ref: taskModel
},
activityId: {
type: Schema.ObjectId,
default: null,
ref: activityModel
},
userId: {
type: ObjectId,
ref: userModel
},
workingDate: {
type: Date
},
dayVal: {
type: Number
},
description: {
type: String
},
});
With virtual fields like:
dataSchema.virtual("project", {
ref: projectModel,
localField: "projectId",
foreignField: "_id",
justOne: true
});
And so on. I would populate each field just like I showed you with the user example. Then, for the timesheet schema I would only reference userId, and populate data like this:
const timesheetSchema = new Schema({
managersComment: {
type: String
},
weekNum: {
type: Number
},
userId: {
type: ObjectId,
ref: dataModel
},
status: {
type: String,
enum: ['Saved', 'Submitted', 'Approved', 'Rejected', 'Reset']
},
}, { timestamps: true });
timesheetSchema.virtual("data", {
ref: dataModel,
localField: "userId",
foreignField: "userId"
});
timesheetSchema.virtual("user", {
ref: userModel,
localField: "userId",
foreignField: "_id",
justOne: true
});
This way you would have a collection with all the imputations for all the users, and you would be able to query and filter that collection for each userId, projectId or anything you would need. Having an array inside your timesheet collection would make this quite more difficult.
One simple solution I found on another SO post is like this:
const result = await timesheetModel.findOne(query).populate({
path: 'data.project data.activity data.task'
});

How do I find object based on child populated property on mongoose

I'm new in mongoose.
I have a Schema like this:
const sessionSchema = mongoose.Schema({
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
registers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Register',
},
],
date: {
type: Date,
default: Date.now,
immutable: true,
},
});
And register model is this one:
const registerSchema = mongoose.Schema({
sets: [
{
weight: {
type: Number,
},
weightUnit: {
type: String,
default: 'kg',
},
repetitions: {
type: Number,
},
duration: {
type: Number,
},
},
],
session: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Session',
},
exercise: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Exercise',
},
creationDate: {
type: Date,
default: Date.now,
immutable: true,
},
});
I want to find all the sessions created by a user and has an register with an specificied exercise id. I tried this:
const result = Session.find({createdBy: userId, 'registers.exercise': exerciseId}).populate('registers');
But doesn't work. ¿Any suggestion?
Thanks :P
Try this:
Session.aggregate([
{
$match: { createdBy: userId }
},
{
$lookup: {
from: 'Register',
localField: 'registers',
foreignField: '_id',
as: 'registers'
}
},
{
$match: { 'registers.exercise': exerciseId }
}
])
.then((result) => {
console.log('Result: ', result);
})
.catch((err) => {
console.log('Error: ', err);
});
If you are not familiar with MongoDB aggregation framework, the query above might be a bit foreign to you. However, for cross collection queries like you described in your question, the aggregation framework is your best shot(for now).
The $match is basically doing what you would do with a Model.find(), while the $lookup is doing what you would do with a Model.<query>.populate()
You can read more about MongoDB aggregation framework here.

get a sub document field in $project stage of aggregation

I'm working with the followings schemas:
Question Schema:
var Question = new Schema({
title: String,
content: String,
createdBy: {
type: Schema.ObjectId,
ref: 'User',
required: true
},
answers: {
type: [ { type: Schema.Types.ObjectId, ref: 'Answer' } ]
}
});
Answer Schema:
var Answer = new Schema({
content:String,
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User',
},
isBest: {
type: Boolean,
default: false
},
createdAt: Date,
votes: Number
});
I'm trying to do an aggregation where I can have as result the list of all questions with a certain fields like title, createdBy (which is going to be populated after the aggregate), an answers_count and a has_best field which is going to be true if a question already have a best answer (an answer with the field isBest equals to true).
I've try with the $project pipeline:
Question.aggregate([{
$project: {
answers_count: { $size: '$answers' },
title: true,
createdAt: true,
createdBy: true,
has_best_answer: '$answers.isBest'
}
}, {
$sort: {
createdAt: -1
}
}], function(err, questions){
if(err){
return res.status(400).send({ message: err });
}
User.populate(questions, { path: 'createdBy', model: 'User', select: 'firstname lastname image' }, function(err, questions){
return res.json(questions);
});
});
Also I've try to $unwind the array and then $lookup for answers but no results when doing an answers_count. This is what I've tried with $unwind and $lookup.
Question.aggregate([
{
$unwind: '$answers'
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answer'
}
},{
$project: {
title: true,
createdBy: true,
createdAt: true,
has_best_answer: '$answer.isBest'
}
}
], function(err, questions){
if(err){
return res.status(400).send({ message: err });
}
User.populate(questions, { path: 'createdBy', model: 'User', select:
'firstname lastname image' }, function(err, questions){
return res.json(questions);
});
});
So, because the $unwind in the array, cannot $size in answers array to do an answers_count field, also when I tried to do $group to having uniques questions id like this:
Question.aggregate([
{
$unwind: '$answers'
},
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answer'
}
},{
$project: {
title: true,
createdBy: true,
createdAt: true,
has_best_answer: '$answer.isBest'
}
},
{
$group: { _id: '$_id' }
}
], function(err, questions){
if(err){
return res.status(400).send({ message: err });
}
User.populate(questions, { path: 'createdBy', model: 'User', select: 'firstname lastname image' }, function(err, questions){
return res.json(questions);
});
});
I have this result:
[
{
"_id": "5825f2846c7ab9ec004f14ce"
},
{
"_id": "5823b9309de40494239c95cd"
},
{
"_id": "582538366062607c0f4bcdaa"
},
{
"_id": "5855d319b6a475100c7beba2"
},
{
"_id": "5878156328dba3d02052b321"
}
]
This is the output that I'm looking for:
[
{
_id: '5825f2846c7ab9ec004f14ce',
title: 'Some question title',
createdBy: '5855d319b6a475100c7beba2', // this is going to be populated,
createdAt: '2016-11-10T00:02:56.702Z',
answers_count: 5,
has_best_answer: true
},
{
_id: '5825f2846c7ab9ec004f14ce',
title: 'Some other question title',
createdBy: '5855d319b6a475100c7beba2', // this is going to be populated,
createdAt: '2016-11-10T00:02:56.702Z',
answers_count: 2,
has_best_answer: false
}
]
You can try something like below.
$lookup - This stage joins all the answers to a question documents.
$project - This stage projects all the required fields. answers_count - Counts the total the number of items in an answers array. has_best_answer - Iterates an answers and compares if any of the isBest field value is true.
Question.aggregate([
{
$lookup: {
from: 'answers',
localField: 'answers',
foreignField: '_id',
as: 'answers'
}
},{
$project: {
title: true,
createdBy: true,
createdAt: true,
answers_count: { $size: '$answers' },
has_best_answer:
{ $anyElementTrue: {
$map: {
input: "$answers",
as: "answer",
in: { $eq: [ "$$answer.isBest", true] }
}
}}
}
}
]);

Resources