how to take result of aggregation/JSON to update a collection document? - javascript

I am making a polling website. Currently I have two collections Polls and Votes. I am using the aggregation pipeline to get the number of votes for each movie. I am having difficulty wrapping my head around updating the poll based on the vote collection. This is the vote Schema:
poll: objectId
votedMovies: Array
0: Object
id: ObjectId
title: string
This is my poll Schema:
_id: ObjectID
pollType: String
totalVotes: Number
movies: Array
0: Object
id: ObjectID
title: String
votes: Number
So far I have an aggregation pipeline that does the following:
let voteCollection = await db.collection('votes').aggregate([
{
$match: {poll: id}
},
{
$unwind: "$votedMovies"
},
{
$group: {_id: "$votedMovies.id", totalVotes: {$sum: 1}}
}
]).toArray()
That spits out something like this:
[{"_id":10674,"totalVotes":2},
{"_id":99861,"totalVotes":1},
{"_id":299534,"totalVotes":4},
{"_id":637157,"totalVotes":3},
{"_id":24428,"totalVotes":5}]
How do I update the poll document so that it has the current number of votes? Am I on the right track with the aggregation pipeline?

You should be able to update each movie votes with:
for (const vote of voteCollection) {
await db.collection('polls').updateOne(
{
_id: id, // pool id
'movies._id': vote._id,
},
{
'movies.$.votes': vote.totalVotes,
}
);
}

Related

$skip and $limit not returning results after first page

I have a relatively simple schema and aggregate system to lookup data for user's outfits, sort them by the number of Likes, Dislikes, or Favorites (each is a collection of user IDs in an array), and then return the data in pages of 12 results at a time. The first page correctly returns 12 objects, but the second page returns none. I have 16 items total that are eligible to be returned (4 should be returned on the 2nd page). Here is my schema:
outfitSchema = new mongoose.Schema({
Items: [{AccessoryType:String,AssetId:mongoose.Schema.Types.Mixed,IsLayered:Boolean,Puffiness:Number,Order:Number}],
Name: String,
Description: String,
Category: String,
Created_At: Number,
Creator: Number,
Likes: [Number],
Dislikes: [Number],
Favorites: [Number]
})
And my aggregate query is as follows:
let true_skip = limit*pageNum
let true_limit = true_skip+limit
query = mongoose.model("Outfit").aggregate([
{ $match: { Category : cat}},
{
$addFields: {
sortLength: {
$size: "$"+sort
}
}
},
{
$sort: {
sortLength: sortBy
}
},
{ $project: {"_id":1}},
{ $skip: true_skip},
{ $limit: limit}
])
limit is always 12 and pageNum iterates up from 0. It may be worth noting that I execute the query at a later point with data = await query. Thanks in advance for any help!

NodeJS Performance with working with arrays

I want to retrieve a user's chats with corresponding users from a different collection in NodeJS and MongoDB.
The nature of NodeJS gives me a bad feeling that running the following code will block or decrease performance of my app. I can duplicate some data but I want to learn more about NodeJS.
Please let me know whether my code is ok and will not decrease performance.
Here I fetch 20 chats. I also need their corresponding users.
then I get the userIds and perform another query against the User collection.
Now I have both but I should merge them using Array.map.
I don't use $lookup because my collections are sharded.
$lookup
Performs a left outer join to an unsharded collection in the same database to filter in documents from the "joined" collection for processing. To each input document, the $lookup stage adds a new array field whose elements are the matching documents from the "joined" collection. The $lookup stage passes these reshaped documents to the next stage.
https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup
let chats = await Chat.find({ active: true }).limit(20);
/*
[
{_id: ..., userId: 1, title: 'Chat A'},
...
]
*/
const userIds = chats.map(item => item.userId);
/*
[1, ...]
*/
const users = await User.find({ _id: { $in: userIds }});
/*
[
{_id: 1, fullName: 'Jack'},
...
]
*/
chats = chats.map(item => {
item.user = users.find(user => user._id === item.userId);
return item;
});
/*
[
{
_id: ...,
userId: 1,
user: {_id: 1, fullName: 'Jack'}, // <-------- added
title: 'Chat A'
},
...
]
*/
This is NOT how you should do it. MongoDB has something called Aggregation Framework and $lookup pipeline that will do that for you automatically with only 1 MongoDB query.
But since you are using Mongoose, this query become even more simpler since you can use populate() method of the Mongoose. So your whole code can be replaced with one line like this:
const chats = await Chat.find({ active: true }).populate('userId;).limit(20);
console.log(chats)
Note: If your collections are sharded, in my opinion you already implemented the logic in best possible way.
You are using async/await, so your code will wait a response from every time use await
// Wait to finish here
let chats = await Chat.find({ active: true }).limit(20);
/*
[
{_id: ..., userId: 1, title: 'Chat A'},
...
]
*/
// Wait to finish here too
const users = await User.find({ _id: { $in: userIds }});
/*
[
{_id: 1, fullName: 'Jack'},
...
]
*/
So if you has too many data and you don't have any index on your collection it will be too long to finish those query.
At this case you should create ref in your collection Chat to collection User with chat.userId = user._id
Then when you call query chat, you populate field userId so you don't have to map const userIds = chats.map(item => item.userId); and chats = chats.map...
Sample for chat schema
const { Schema, model } = require("mongoose");
const chatSchema = new Schema({
active: Boolean,
userId: {
type: "ObjectId",
ref: "User",
},
title: String,
message: String
// another property
});
const userSchema = new Schema({
username: String,
email: String
// another property
})
// query for chat
const chatModel = new model('chat', chatSchema)
let chats = await chatModel.find({ active: true }).populate('userId').limit(20);
/*
[
{
_id: ...,
userId: {_id: 1, fullName: 'Jack'}, // <-------- already have
title: 'Chat A'
},
...
]
*/

Is there a way to update an object in an array of a document by query in Mongoose?

I have got a data structure:
{
field: 1,
field: 3,
field: [
{ _id: xxx , subfield: 1 },
{ _id: xxx , subfield: 1 },
]
}
I need to update a certain element in the array.
So far I can only do that by pulling out old object and pushing in a new one, but it changes the file order.
My implementation:
const product = await ProductModel.findOne({ _id: productID });
const price = product.prices.find( (price: any) => price._id == id );
if(!price) {
throw {
type: 'ProductPriceError',
code: 404,
message: `Coundn't find price with provided ID: ${id}`,
success: false,
}
}
product.prices.pull({ _id: id })
product.prices.push(Object.assign(price, payload))
await product.save()
and I wonder if there is any atomic way to implement that. Because this approach doesn't seem to be secured.
Yes, you can update a particular object in the array if you can find it.
Have a look at the positional '$' operator here.
Your current implementation using mongoose will then be somewhat like this:
await ProductModel.updateOne(
{ _id: productID, 'prices._id': id },//Finding Product with the particular price
{ $set: { 'prices.$.subField': subFieldValue } },
);
Notice the '$' symbol in prices.$.subField. MongoDB is smart enough to only update the element at the index which was found by the query.

Use a Mongo Cursor to build a list with another query

I have two mongo collections.
Enrollment:
{UserID: String, CourseID: String, EducatorFlag: boolean}
Courses
{_id: String, courseName: String}
I'm attempting to generate a list of courseNames when given a UserID. This requires me to find all courses that a User is enrolled in. The following function returns just the CourseID of each course a user is in.
var currentCourses = Enrollment.find(
{ UserId: Meteor.userId(), EducatorFlag: false },
{ fields: { CourseID: 1 });
I'm unsure of how to take this cursor, and use each item in it to run another query and build a list from the output. Basically for each CourseID in currentCourses I need to do
var result = []
result += Courses.find({_id: CourseID}, {fields: {_id: 0, courseName: 1}});
The goal is simply to print all the courses that a user is enrolled in.
You have several options:
Use the cursor directly with a .forEach()
Use .fetch() to transform the cursor into an array of objects and then manipulate that.
Get an array of _ids of enrollments with .map() and directly search the courses with mongo's $in
Let's just use the first one for now since it's pretty simple:
let courseNames = [];
Enrollment.find(
{ UserId: Meteor.userId(), EducatorFlag: false },
{ fields: { CourseID: 1 }).forEach((e)=>{
let course = Courses.findOne(e.CourseID, { fields: { courseName: 1 }})
courseNames.push(course.courseName);
});
Note: when selecting fields in a query you can't mix exclusions and inclusions.
Getting an array of _ids and using that with $in is also pretty straightforward:
let courseIdArray = Enrollment.find(
{ UserId: Meteor.userId(), EducatorFlag: false },
{ fields: { CourseID: 1 }).map((e)=>{ return e.CourseID });
let courseNames = Courses.find(
{ _id: { $in: courseIdArray }}).map((c)=>{ return c.courseName });

mongodb aggregate with find features

I have a model similar to this one:
{
email: String,
name: String,
role: String,
location: {
country: String,
city: String
},
contacts: {
email: String,
phone: String
}
}
I need to show in my view the entire users information but I wish to include also how many users from a country there are.
Using aggregate I don't know how to get the full user over the groups I create.
So at the moment what I'm doing is this:
User.find({}, function(err, users) {
User.aggregate([
{ $group: { _id: { country: '$location.country' }, count: { $sum: 1 }}}
], function(err, results) {
res.render('home', {
users: users,
countries: results
});
});
});
As you can see I'm using Find and then aggregate to get both the information I need... but I'm pretty sure there is a way to get it using only aggregate but I can not find how to do that...
If you need to accumulate the entire user information for each group, then you need to use the $push operator for accumulation and the $$ROOT system variable to access the entire user info.
User.aggregate([
{$group:{"_id":{"country":"$location.country"},
"users":{$push:"$$ROOT"},
"count":{$sum:1}}}
],callback)
In case you would want to accumulate only specific fields of the user information document, you could just push the required fields like:
User.aggregate([
{$group:{"_id":{"country":"$location.country"},
"users":{$push:{"email":"$email","name":"$name"}},
"count":{$sum:1}}}
],callback)

Categories

Resources