MongoDB aggregation: Check if user has already rated a product - node.js

I'm having some trouble coming up with a solution to this problem by using mongo directly instead of node.
I have a collection of products that have several fields, including an array of ratings. The ratings consist of the id of the user who rated it and the score. They look like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"allRatings": [
{
"user_id": "6282962e3aa8163084776092",
"rating": 9
},
{
"user_id": "62811d520f52b64990a94e99",
"rating": 6
}
],
}
I'd like to return all of the fields, but instead of all the ratings, I just want to return a boolean that shows if the current user, whose ID I have in the code, has already rated the product. I have tried with aggregations, but haven't found any solution that works.
Something like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"hasRated": true,
}

If I've understood correctly you can use this aggregation query:
This query looks for the object you want (matching by _id) and then, into a $project stage it set the value hasRated if the desired id exists into the array.
db.collection.aggregate([
{
$match: {
_id: ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
"6282962e3aa8163084776092",
"$allRatings.user_id"
]
}
}
}
])
Example here
To do in JS you can simply create the object adding the id as a variable (something like this, is pseudocode, not tested):
const id = 'your_id_here'
const agg = [{
$match: {
_id: new ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
id,
"$allRatings.user_id"
]
}
}
}]
db.aggregate(agg)

Related

Mongoose conditional update with array of objects

I have a Cart schema in Mongoose including CartItems as an array of objects.
{
_id: string;
items: [
{
product: string;
options: {
size: string;
color: string;
}
quantity: number;
}
]
}
In this case, is it possible to push an item when product and options doesn't match or add quantity when it does?
Here is the code that I have tried which does not work.
Cart.findByIdAndUpdate(_id, {
items: {
$cond: [
{
// Does not work
$elemMatch: {
product,
"options.color": options.color,
"options.size": options.size,
},
},
{
// Should add quantity where it is matched.
},
{
$push: {
product,
productName,
options,
quantity,
},
},
],
},
});
Query
pipeline update requires MongoDB >= 4.2
newProduct is the product you want to add (a js variable)
check if product already exists => add not-exist? field
if not-exists add it in the end
else map to find it and update the quantity
unset the 2 fields newProduct and not-exists
*it does 2 array reads, alternative could be to use $reduce but if you have many products $concatArrays is slow to be inside a reduce, so this is faster solutions (even if reduce would read the array 1 time only)
*you need a method to do update with pipeline, i don't know if mongoose is updated to support it, we are MongoDB 5 so i guess it will be(java is), in worst case you can use updateCommand and call it with runCommand(...)
Test code here
update({"_id" : "1"},
[{"$set":
{"newProduct":
{"product":"p1",
"options":{"size":"s1", "color":"c1"},
"quantity":1}}},
{"$set":
{"not-exists?":
{"$eq":
[{"$filter":
{"input":"$items",
"cond":
{"$and":
[{"$eq":["$$this.product", "$newProduct.product"]},
{"$eq":["$$this.options.size", "$newProduct.options.size"]},
{"$eq":["$$this.options.color", "$newProduct.options.color"]}]}}},
[]]}}},
{"$set":
{"items":
{"$cond":
["$not-exists?", {"$concatArrays":["$items", ["$newProduct"]]},
{"$map":
{"input":"$items",
"in":
{"$cond":
[{"$and":
[{"$eq":["$$this.product", "$newProduct.product"]},
{"$eq":["$$this.options.size", "$newProduct.options.size"]},
{"$eq":["$$this.options.color", "$newProduct.options.color"]}]},
{"$mergeObjects":
["$$this", {"quantity":{"$add":["$$this.quantity", 1]}}]},
"$$this"]}}}]}}},
{"$unset":["not-exists?", "newProduct"]}])
Query2
if you don't want to use update pipeline you can do it with more queries
Check if exists
db.collection.find({
"_id" : "1",
"items": {
"$elemMatch": {
"product": "p1",
"options": {
"size": "s1",
"color": "c1"
}
}
}
})
If not exists
db.collection.update({
"_id": "1"
},
{
"$push": {
"items": "NEWITEM" //put here the new object
}
})
else If exists
db.collection.update({"_id" : "1"},
{
"$inc": {
"items.$[i].quantity": 1
}
},
{
arrayFilters: [
{
"i.product": "p1",
"i.options.size": "s1",
"i.options.color": "c1"
}
]
})

Fetch grouped items from database

I'm trying to group values together in mongoose. I have a "Review" schema with the following fields:
{ userId, rating, comment }
There are many documents with the same userId. How can I retrieve them in the following format:
{userId: [...allRatings]
Or even better, is there a way to retrieve the averages for each userId? so like this: {userId: 2.8}
I know it's possible and very simple to do in node.js, but is there a way of doing it with mongoose?
Mongoose is really just a vehicle to pass commands to your mondoDB server, so accomplishing what you want in mongoose isn't dissimilar to accomplishing it in the mongo shell.
Here is the aggregation you're looking for:
db.collection.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
The first stage of the pipeline groups all ratings by useId. The second stage calculates the ratings average and pretties up the key display. That's it. The result will be this:
[
{
"avgRating": 2.8,
"userId": 110
},
{
"avgRating": 3.275,
"userId": 100
}
]
Here is a playground for you: https://mongoplayground.net/p/yXVxk4klabB
As for how to specifically run this command in mongoose, well that's pretty straightforward:
const YourModel = mongoose.model('your_model');
...
YourModel.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
.then(result => {
console.log(result);
})

Followers - mongodb query check

I have 2 collections in my database. One is called User
{
_id: storeUserId,
name: ...,
etc
}
the other one called Following
{
userId: ...,
followingUserId: ...
}
the userId is the current user id and the followingUserId is the id that current user wants to follow.
For example, in User collection I have:
{
_id: userIdOne,
etc
},
{
_id: userIdTwo,
etc
}
and in Following collection I have:
{
userId: userIdThousand,
followingUserId: userIdTwo
}
When I run find query
db.bios.find();
I get
{
"_id": userIdTwo,
"username": "random27005688"
},
{
"_id": userIdThree
"username": "random232111"
},
{
"_id": userIdOne
"username": "random2702"
}
]
The result is what I want but I want to add a 'isFollowed' field to each result item to check following status. I have a user id let say: 'userIdThousand' which I want to use it to check against each result item based on my Following collection. E.g,
check if userIdThousand is following userIdOne
check if userIdThousand is following userIdTwo, etc.
Below is my expected result. Thanks!
[
{
_id: userIdTwo,
"username": "random27005688",
"isFollowed": true
},
{
"_id": userIdThree
"username": "random232111",
"isFollowed": false
},
{
"_id": userIdOne
"username": "random2702",
"isFollowed": false
},
]
You need $lookup to get the data from second collection matching by followingUserId then you can use $filter to get only followers with particular _id and check if new array has any elements (using $size) which means that user is followed by other user:
db.User.aggregate([
{
$match: {
_id: { $ne: "userIdOne" }
}
},
{
$lookup: {
from: "Following",
localField: "_id",
foreignField: "followingUserId",
as: "followers"
}
},
{
$addFields: {
followers: {
$filter: { input: "$followers", as: "follower", cond: { $eq: [ "$$follower._id", "userIdOne" ] } }
}
}
},
{
$project: {
_id: 1,
username: 1,
isFollowed: { $gt: [ { $size: "$followers" }, 0 ] }
}
}
])
maybe you could split it into two step.
1. query User and get the result
for example, you get a user.
{
_id: userIdTwo,
"username": "random27005688",
}
2. query Following and get if the user has been followed.
such as
has_followed=db.Following.find({"userId":userIdTwo}).count()
not the best solution but it might help u.

Search by Date within an Object in Array

I have the following Mongoose schema:
let ExerciserSchema = new Schema({
username: {
type: String,
required: true
},
exercises: [{
desc: String,
duration: Number,
date: {
type: Date,
default: new Date()
}
}]
});
I want to search by username and limit the exercise results to a date range.
I tried this lookup function:
let user = await Exerciser.find(
{ "username": name },
{ "exercises.date": { "$gte": from }},
{ "exercises.date": { "$lte": to }}
).exec((err, data) => {
if (err) {
res.json({ Error: "Data not found" })
return done(err);
}
else {
res.json(data);
return done(null, data);
}
});
However, it's logging an error and not returning the data.
MongoError: Unsupported projection option: exercises.date: { $gte: new Date(1526342400000) }
I realize from that error it appears like my date is being searched for in milliseconds, but I console.log it right before I run the above function and it's in date mode, which is what I think I want: 2018-05-01T00:00:00.000Z
How can I make this work so that I can search by a date range given my Schema? I can change the format of the date in the Schema if necessary. I'd just like the simplest solution. Thanks for your help.
You're query is wrong. You were trying to write an AND condition, but you separated documents instead of putting everything into one. This means the "second" argument to Model.find() was interpreted as a a "projection of fields", hence the error:
MongoError: Unsupported projection option:
So it's not a "schema problem" but that you sent the wrong arguments to the Model.find() method
Also you need $elemMatch for multiple conditions on elements within an array:
// Use a try..catch block with async/await of Promises
try {
let user = await Exerciser.find({
"username": name,
"exercises": {
"$elemMatch": { "date": { "$gte": from, "$lte": to } }
}
});
// work with user
} catch(e) {
// handle any errors
}
Most importantly you don't await a callback. You either await the Promise like I am showing here or simply pass in the callback instead. Not both.
Exerciser.find({
"username": name,
"exercises": {
"$elemMatch": { "date": { "$gte": from, "$lte": to } }
}
}).exec((err,user) => {
// the rest
})
FYI, what you were attempting to do was this:
Exerciser.find({
"$and": [
{ "username": name },
{ "exercises.date": { "$gte": from }},
{ "exercises.date": { "$lte": to }}
]
)
But that is actually still incorrect since without the $elemMatch the $gte and $lte applies to ALL elements of the array and not just a single one. So the incorrect results would show if ANY array item was less than the date but not necessarily greater than.
For array elements the $elemMatch enforces the "between" of the two conditions.
I managed to get it. This answer matches by username, and filters exercises so they are between the dates with variable names to and from. This is what I had wanted.
let user = Exerciser.aggregate([
{ $match: { "username": id }},
{ $project: { // $project passes along the documents with the requested fields to the next stage in the pipeline
exercises: { $filter: {
input: "$exercises",
as: "exercise",
cond: { $and: [
{ $lte: [ "$$exercise.date", to ] },
{ $gte: [ "$$exercise.date", from ] },
]}
}},
username: 1, // include username in returned data
_id: 0
}}
])
Result:
[
{
"username": "scott",
"exercises": [
{
"desc": "Situps",
"duration": 5,
"_id": "5af4790fd9a9c80c11aac696",
"date": "2018-04-30T00:00:00.000Z"
},
{
"desc": "Situps",
"duration": 10,
"_id": "5afb3f03e12e38020d059e67",
"date": "2018-05-01T00:00:00.000Z"
},
{
"desc": "Pushups",
"duration": 8,
"_id": "5afc08aa9259ed008e7e0895",
"date": "2018-05-02T00:00:00.000Z"
}
]
}
]

Use $addToSet (aggregate) to add objects to an array with one or more distinct properties

function findProfilesBySteamIds(ids, res) {
var match = {
$match: {
$and : [
{ steamid: { $in : ids } }
]
}
}
var sort = {
$sort: {
createdAt : -1
}
}
var group = {
$group: {
_id : "$steamid",
profile : { $first : "$$ROOT" },
personahistory : { $push : "$$ROOT" }
}
}
SteamUser
.aggregate([match, sort, group])
}
Okay so here is my issue. I have a collection of profiles with a unique identifier steamid. I am grouping by $steamid and that functionality is working as expected. I also want to add a new field (not in the Schema) called personahistory that has an array of objects that is DISTINCT based on another property of the document, called personaname.
I have tried to use $addToSet following the mongoose reference docs but so far can only use it to create an array of that property only:
$group: {
_id : "$steamid",
profile : { $first : "$$ROOT" },
personahistory : { $addToSet : "$personaname" }
}
This outputs:
{
"_id": "1234567890",
"profile": { ... },
"personahistory" : [
"personaname1",
"personaname2",
"personaname3"
]
}
Instead I'd like the output to be along the lines of:
{
"_id": "1234567890",
"profile": { ... },
"personahistory" : [
{
"_id": "1234567890",
"personaname": "personaname1",
...
},
{
"_id": "1234567891",
"personaname": "personaname2",
...
},
{
"_id": "1234567892",
"personaname": "personaname3",
...
}
]
}
I've tried something like:
{
$addToSet : {
"_id": "$steamid",
"personaname": "$personaname"
}
}
but to no avail.
Furthermore, if this behavior is even possible, I'd like to be able to use two or more DISTINCT fields. So, get all unique combinations of personaname and avatarurl and add those to the set.
Basically, I have a record each time the profile is queried but I only want to return records that are unique based on those two fields.
I'll be happy to provide more information if I haven't been clear enough.

Resources