MongoDB aggregate claculating the outdated collection - javascript

I'm working on a social media app and I'm having this problem, I have a collection for friends to store friend requests and the relationship between users and everything works fine, but I'm working on a post('save') middleware and aggrgate pipeline to count the number of friends per user per process which also worked But the problem is aggrgate pipeline counting friends before update collection eg if user accepts friend request it counts friends without this new friend.
Is there any solution to this or best way to do it?
Here is the code
const mongoose = require('mongoose');
const User = require('./userModel');
const FriendSchema = new mongoose.Schema({
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
recipient: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
status: {
type: String,
enum: ['pending', 'accepted', 'cancelled'],
required: true,
},
createdAt: { type: Date, default: Date.now },
});
FriendSchema.statics.calcFriends = async function (senderID, recipientID) {
const senderIDstats = await this.aggregate([
{
$match: {
$or: [
{ sender: senderID, status: 'accepted' },
{ recipient: senderID, status: 'accepted' },
],
},
},
{
$group: {
_id: null,
count: { $sum: 1 },
},
},
]);
const recipientIDstats = await this.aggregate([
{
$match: {
$or: [
{ sender: recipientID, status: 'accepted' },
{ recipient: recipientID, status: 'accepted' },
],
},
},
{
$group: {
_id: null,
count: { $sum: 1 },
},
},
]);
await User.bulkWrite([
{
updateOne: {
filter: { _id: senderID },
update: {
friendsCount: senderIDstats.length > 0 ? senderIDstats[0]?.count : 0,
},
},
},
{
updateOne: {
filter: { _id: recipientID },
update: {
friendsCount:
recipientIDstats.length > 0 ? recipientIDstats[0]?.count : 0,
},
},
},
]);
};
FriendSchema.index({ sender: 1, recipient: 1 }, { unique: true });
FriendSchema.post('save', function () {
this.constructor.calcFriends(this.sender, this.recipient);
});
const Friend = mongoose.model('Friend', FriendSchema);
module.exports = Friend;

Related

How can I push a value to a nested array by using a put request in MongoDB?

How can I add the "userid" (its a string) to the nested array "people_attending" in MongoDB ?
The problem is that I can not add to the array people_attending.
Here is my MongoDB schema:
const OrganizationSchema = new Schema({
name: {
type: String,
required: true,
unique: true,
},
register_date: {
type: Date,
default: Date.now,
},
teams: [
{
sport: {
type: String,
required: false,
},
events: [
{
date_time: {
type: Date,
offset: true,
},
opponent: {
type: String,
required: true,
},
people_attending: [String],
amenities: [String],
},
],
},
],
});
Here is my attempt:
I would like to find the event that equals to the given eventid and add the userid to the people_attending array.
router.put("/event/attend/:orgid/:teamid/:eventid/:userid", (req, res) => {
const userid = req.params.userid;
const eventid = req.params.eventid;
const teamid = req.params.teamid;
const orgid = req.params.orgid;
console.log(teamid, userid, eventid);
Organization.findOneAndUpdate(
{ _id: orgid, "teams._id": teamid, "teams.events._id": eventid },
{
$addToSet: {
"teams.$.events.$.people_attending": userid,
},
}
)
.then((team) => {
res.status(200).json(team);
})
.catch((err) => {
res.status(400).json(err);
});
});
Found the solution:
Organization.findOneAndUpdate(
{ _id: orgid, "teams._id": teamid, "teams.events._id": eventid },
{
$addToSet: {
"teams.$.events.$[].people_attending": userid,
},
},
{ new: true }

How to group mongoose results by status, and populate a reference field?

I have a list of task records (see schema below). I am attempting to return records for a specific projectId, group my task records by status, and populate the responsible field. However, the responsible field is not populating. I have attached a code snippet below. Can anyone advise what I am doing incorrectly?
Code Snippet:
const test = await Task.aggregate([
{
$match: { project: { $eq: mongoose.Types.ObjectId(projectId) } },
},
{
$group: {
_id: "$status",
data: {
$push: {
_id: "$status",
name: "$name",
responsible: "$responsible",
endDate: "$endDate",
},
},
},
},
{
$sort: { status: 1 },
},
]);
console.log("test1:", test);
// Populate Aggregated Data:
const tasks = await User.populate(test, { path: "data.responsible" });
console.log("test2:", tasks);
TaskSchema:
const TaskSchema = new mongoose.Schema({
name: {
type: String,
trim: true,
required: true,
},
responsible: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
endDate: {
type: Date,
required: false,
},
status: {
type: String,
enum: ["new", "active", "inactive", "closed"],
required: true,
},
project: {
type: mongoose.Schema.Types.ObjectId,
ref: "Project",
},
});
Desired Results:
results = [
{
_id: “new”
data: [
{
endDate: "2022-09-16T04:00:00.000Z”,
name: "test1”,
responsible: {
email: “jane.doe#ymail.com”,
firstName: “Jane”,
lastName: “Doe”
},
{
endDate: "2022-09-16T04:00:00.000Z”,
name: "test2”,
responsible: {
email: “john.doe#ymail.com”,
firstName: “John”,
lastName: “Doe”
},
]
},

]

Looping Over And Array of Objects and Updating Each One

I'm trying to update objects inside an array based on specific conditions that I check using two functions. I couldn't achieve this with the match feature available in mongoose.
As you can see below, I'm trying to loop over the array of posts, send each post and check if it fulfills a condition, then update it using findOneAndUpdate. The functions isLessThanOne and isAchieved return the expected value.
(async () => {
let posts = await Post.find()
posts.map(async (post) => {
if (isLessThanOneDay(post.deadline)) {
console.log(post.id, post._id)
await Post.findOneAndUpdate(post.id, { $set: { category: 'deadline' }})
} else if (isAchieved(post.milestones)) {
await Post.findOneAndUpdate(post.id, { $set: { category: 'achieved' }})
} else {
await Post.findOneAndUpdate(post.id, { $set: { category: 'newGoals' }})
}
})
})()
This is the schema
const { type } = require("express/lib/response")
const mongoose = require("mongoose")
const PostSchema = new mongoose.Schema({
// we have to link or refer each post to a user
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user",
},
title: {
type: String,
required: true
},
desc: {
type: String
},
milestones: [],
likes: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
fName: {
type: String,
required: true
},
date: {
type: String,
required: true
}
}
],
comments: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
text: {
type: String,
required: true
},
fName: {
type: String,
},
date: {
type: String,
}
}
],
rewards: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
fName: {
type: String,
required: true
},
date: {
type: String,
required: true
},
reward: {
type: String,
required: true
},
price: {
type: Number,
required: true
}
}
],
date: {
type: String,
required: true
},
shares: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user"
},
fName: {
type: String,
required: true
},
date: {
type: String,
required: true
}
}
],
deadline: {
type: String,
required: true
},
category: {
type: String,
}
})
module.exports = Post = mongoose.model('post', PostSchema)
The problem is that I couldn't update the posts and I've already tried using udpate, directly updating it with post.category then save. Any help would appreciated.
Try this:
(async () => {
let posts = await Post.find()
posts.map(async (post) => {
if (isLessThanOneDay(post.deadline)) {
console.log(post.id, post._id)
await Post.findOneAndUpdate({ _id: post.id }, { $set: { category: 'deadline' }})
} else if (isAchieved(post.milestones)) {
await Post.findOneAndUpdate({ _id: post.id }, { $set: { category: 'achieved' }})
} else {
await Post.findOneAndUpdate({ _id: post.id }, { $set: { category: 'newGoals' }})
}
})
})()
If I recall, the filter parameter must be an object.

Mongoose virtual environment doesn't appear in postman

I have added a virtual environment for reviews but for some reason it doesn't appear in postman when i try to get it. If worked correctly it should display reviews with a value of null. I am fairly new to this but i did read through the mongoose documentation and other online sources but everything seems similar to what i wrote.
const mongoose = require('mongoose');
const slugify = require('slugify');
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name required'],
unique: true,
maxlength: [40, 'Less then or equal to 40 characters'],
minlength: [10, 'More then or equal to 10 characters'],
},
slug: String,
duration: {
type: Number,
required: [true, 'Duration required'],
},
maxGroupSize: {
type: Number,
required: [true, 'Group size required'],
},
difficulty: {
type: String,
required: [true, 'Difficulty required'],
enum: {
values: ['easy', 'medium', 'difficult'],
message: 'Difficulty is either easy, medium, difficult',
},
},
ratingsAverage: {
type: Number,
default: 4.5,
max: [5, 'Less than or equal to 5'],
min: [1, 'More than or equal to 1'],
},
ratingsQuantity: {
type: Number,
default: 0,
},
price: {
type: Number,
required: [true, 'Price required'],
},
priceDiscount: {
type: Number,
validate: {
validator: function (val) {
// This only points to current doc on New doc creating
return val < this.price ? true : false;
},
message: 'Discount should be less than the regular price',
},
},
summary: {
type: String,
trim: true,
required: [true, 'Summary required'],
},
description: {
type: String,
trim: true,
},
imageCover: {
type: String,
required: [true, 'Image required'],
},
images: [String],
createAt: {
type: Date,
default: Date.now(),
},
startDates: [Date],
secretTour: {
type: Boolean,
default: false,
},
startLocation: {
// GeoJSON (At least 2 field names e.g. type and coordinates)
type: {
type: String,
default: "Point",
enum: ["Point"]
},
coordinates: [Number],
address: String,
description: String,
},
locations: [{
type: {
type: String,
default: "Point",
enum: ["Point"],
},
coordinates: [Number],
address: String,
description: String,
day: Number
}],
guides: [{
type: mongoose.Schema.ObjectId,
ref: "User"
}]
}, {
toJSON: {
virtuals: true
},
toObject: {
virtuals: true
}
});
tourSchema.virtual('durationWeeks').get(function () {
return this.duration / 7;
});
// Virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
foreignField: 'tour',
localField: '_id',
});
Here is the controller for the tour
const Tour = require('../models/tourModel');
const APIFeatures = require('../utils/apiFeatures');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');
const {
deleteOne,
updateOne,
createOne
} = require("./handlerFactory")
const {
populate
} = require('../models/tourModel');
const aliasTopTours = catchAsync(async (req, res, next) => {
req.query.limit = '5';
req.query.sort = '-ratingAverage,price';
req.query.fields = 'name, price, ratingAverage, summary, difficulty';
next();
});
const getAllTours = catchAsync(async (req, res, next) => {
// Execute query
const features = new APIFeatures(Tour.find(), req.query)
.filter()
.sort()
.limit()
.pagination();
const tours = await features.query;
// Send response
res.status(200).json({
status: 'Success',
length: tours.length,
message: tours,
});
});
const getTour = catchAsync(async (req, res, next) => {
const tourId = await (await Tour.findById(req.params.Id)).populate('reviews');
if (!tourId) {
return next(new AppError('No tour found with that ID', 404));
}
res.status(200).json({
status: 'Success',
data: {
tourId,
},
});
});
const postTour = createOne(Tour)
const patchTour = updateOne(Tour)
const deleteTour = deleteOne(Tour)
const getTourStats = catchAsync(async (req, res, next) => {
const stats = await Tour.aggregate([{
$match: {
ratingAverage: {
$gte: 4.5,
},
},
},
{
$group: {
_id: '$difficulty',
aveRating: {
$avg: '$ratingAverage',
},
avePrice: {
$avg: '$price',
},
minPrice: {
$min: '$price',
},
maxPrice: {
$max: '$price',
},
totalRating: {
$sum: '$ratingQuantity',
},
totalTours: {
$sum: 1,
},
},
},
{
$sort: {
avePrice: 1,
},
},
]);
res.status(200).json({
status: 'Success',
message: stats,
});
});
const getMonthlyPlan = catchAsync(async (req, res, next) => {
const year = req.params.year * 1;
const plan = await Tour.aggregate([{
$unwind: '$startDates',
},
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`),
},
},
},
{
$group: {
_id: {
$month: '$startDates',
},
numTourStarts: {
$sum: 1,
},
tours: {
$push: '$name',
},
},
},
{
$addFields: {
month: '$_id',
},
},
{
$project: {
_id: 0,
},
},
{
$sort: {
numTourStarts: -1,
},
},
{
$limit: 12,
},
]);
res.status(200).json({
status: 'Success',
length: plan.length,
message: plan,
});
});
module.exports = {
getAllTours,
getTour,
postTour,
patchTour,
deleteTour,
aliasTopTours,
getTourStats,
getMonthlyPlan,
};
const tourId = await (await Tour.findById(req.params.Id)).populate('reviews');
The problem is here, you need to remove one await here, no need to use 2 await in this line

Mongoose, apply `.aggregate()` on array of subdocuments and get the result with other fields of document in least number of queries

This is my Mongoose Model:
const postSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
caption: {
type: String
},
action: {
type: [{
actionName: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}],
default: []
},
shares: [{
type: Schema.Types.ObjectId,
ref: 'User'
}];
});
All I want is to have a mongodb query with or without using .aggregate() to get the user & caption field as it is but instead of action and shares I want their counts for a particular document.
Sample Document
{
_id: "fgsergehegoieofgesfglesfg",
user: "dfjksjdnfkjsdfkjsdklfjglkbj",
caption: "This is the post caption",
action: [
{
actionName: 'e1', user: "sdfasdsdfasdfdsdfac951e5c"
},
{
actionName: 'e1', user: "asdfmadfadfee103c9c951e5d"
},
{
actionName: 'e2', user: "op34937cdbae0cd4160bbec"
},
{
actionName: 'e2', user: "2543ebbasdfd1750690b5b01c"
},
{
actionName: 'e3', user: "asdfcfebdb5dd1750690b5b01d"
},
],
shares: ["ewrebdb5ddadsf5069sadf1d", "asdfsdfbb85dd1750690b5b01c", "fasec92dsfasde103c9c95df5d"]
};
Desired output after query:
{
_id: "fgsergehegoieofgesfglesfg",
user: 'dfjksjdnfkjsdfkjsdklfjglkbj',
caption: 'This is the post caption',
actionCount: [{ count: 1, actionName: 'e3' },
{ count: 2, actionName: 'e2' },
{ count: 2, actionName: 'e1' }],
shareCount: 3
}
I am able do get following results using .aggregate():
Query:
let data = await Test.aggregate([
{ $match: { _id: mongoose.Types.ObjectId("fgsergehegoieofgesfglesfg") } },
{ $unwind: "$action" },
{
$group: {
_id: "$action.actionName",
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
actionName: "$_id",
count: 1
}
}
]);
Result:
[
{ count: 1, actionName: 'e3' },
{ count: 2, actionName: 'e2' },
{ count: 2, actionName: 'e1' }
]
I just want to put this in the original document and get the result. Also, doing the same for share field. It would be better if this can be done in single query. I have tried using $replaceRoot along with $mergeObjects but don't know how to correctly use them. I am very new to mongodb and mongoose.
Please help. Thank you.
Since you're aggregating a nested array you need to run $grouptwice and $first can be used to preserve original document's field values:
await Test.aggregate([
{ $match: { _id: mongoose.Types.ObjectId("fgsergehegoieofgesfglesfg") } },
{ $unwind: "$action" },
{
$group: {
_id: { _id: "$_id", actionName: "$action.actionName" },
user: { $first: "$user" },
caption: { $first: "$caption" },
count: { $sum: 1 },
shareCount: { $first: { $size: "$shares" } }
}
},
{
$group: {
_id: "$_id._id",
user: { $first: "$user" },
caption: { $first: "$caption" },
shareCount: { $first: "$shareCount" },
actionCount: {
$push: {
actionName: "$_id.actionName",
count: "$count"
}
}
}
}
])
Mongo Playground

Categories