Mongoose aggregation with $sum returning 0? - node.js

I know several question have been asked in here but no one give me the correct answer. So basically I have Schema like here
const cashFlowSchema = new Schema({
date: { type: Date, default: Date.now },
type: { type: String, required: true },
category: { type: String, required: true },
amount: { type: Number, required: true },
description: String
});
And then I tried to create get sum for amount with aggregate like this
CashFlow.aggregate([{
$group: {
_id: null,
balance: { $sum: "$amount" }
}
}], function(err, result) {
if (err) {
console.log(err);
return;
}
console.log(result);
});
And give the result like this
[ { _id: null, balance: 0 } ]
But if I try in Robo3T (previously Robomongo), it give me correct answer. I use mongodb version 3.4. Thank you before.
Update
I found the answer, so it's my silly mistake. After I enable debug mode in mongoose, it show I'm make wrong connection to the collection. After fix it, I get the correct result. I will delete this question soon, thank's all before.

Related

How to query for sub-document in an array with Mongoose

I have a Schema of Project that looks like this:
const ProjectSchema = new mongoose.Schema({
name: {
type: String,
Required: true,
trim: true
},
description: {
type: String,
},
devices: [{
name: {type: String, Required: true},
number: {type: String, trim: true},
deck: {type: String},
room: {type: String},
frame: {type: String}
}],
cables: {
type: Array
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
adminsID: {
type: Array
},
createdAt: {
type: Date,
default: Date.now
}
I want to query an object from array of "devices".
I was able to add, delete and display all sub-documents from this array but I found it really difficult to get single object that matches _id criteria in the array.
The closest I got is this (I'm requesting: '/:id/:deviceID/edit' where ":id" is Project ObjectId.
let device = await Project.find("devices._id": req.params.deviceID).lean()
console.log(device)
which provides me with below info:
[
{
_id: 6009cfb3728ec23034187d3b,
cables: [],
adminsID: [],
name: 'Test project',
description: 'Test project description',
user: 5fff69af08fc5e47a0ce7944,
devices: [ [Object], [Object] ],
createdAt: 2021-01-21T19:02:11.352Z,
__v: 0
}
]
I know this might be really trivial problem, but I have tested for different solutions and nothing seemed to work with me. Thanks for understanding
This is how you can filter only single object from the devices array:
Project.find({"devices._id":req.params.deviceID },{ name:1, devices: { $elemMatch:{ _id:req.params.deviceID } }})
You can use $elemMatch into projection or query stage into find, whatever you want it works:
db.collection.find({
"id": 1,
"devices": { "$elemMatch": { "id": 1 } }
},{
"devices.$": 1
})
or
db.collection.find({
"id": 1
},
{
"devices": { "$elemMatch": { "id": 1 } }
})
Examples here and here
Using mongoose is the same query.
yourModel.findOne({
"id": req.params.id
},
{
"devices": { "$elemMatch": { "id": req.params.deviceID } }
}).then(result => {
console.log("result = ",result.name)
}).catch(e => {
// error
})
You'll need to use aggregate if you wish to get the device alone. This will return an array
Project.aggregate([
{ "$unwind": "$devices" },
{ "$match": { "devices._id": req.params.deviceID } },
{
"$project": {
name: "$devices.name",
// Other fields
}
}
])
You either await this or use .then() at the end.
Or you could use findOne() which will give you the Project + devices with only a single element
Or find, which will give you an array of object with the _id of the project and a single element in devices
Project.findOne({"devices._id": req.params.deviceID}, 'devices.$'})
.then(project => {
console.log(project.devices[0])
})
For now I worked it around with:
let project = await Project.findById(req.params.id).lean()
let device = project.devices.find( _id => req.params.deviceID)
It provides me with what I wanted but I as you can see I request whole project. Hopefuly it won't give me any long lasting troubles in the future.

I wanna fetch all users who have most sparks in last 7 days in mongodb and nodejs

I wanna fetch all users who has most sparks in last 7 days
Sparks are something like friends
can anyone help in this problem
Thanks in advance
//create schema for users
const UserSchema = new Schema({
password: {
type: String,
default: "",
},
account_type: {
type: String,
default: "",
},
account_name: {
type: String,
default: "",
},
firstName: {
type: String,
default: "",
},
lastName: {
type: String,
default: "",
},
image: {
type: String,
default: "",
},
sparks: [
{
user: {
type: Schema.Types.ObjectId,
ref: "users",
},
status: {
type: Number,
required: true,
enum: STATUS,
},
createdAt: {
type: Date,
default: Date.now,
},
},
],
createdAt: {
type: Date,
default: Date.now,
},
});
This is my schema
router.get("/leaderboard/get/sparks", async (req, res) => {
let newDate = moment().subtract(7, "day");
console.log(
Date("2020-06-26T12:49:29.324Z") < Date("2020-06-24T12:49:29.563+00:00")
);
let topUsers = await User.aggregate([
{
$match: {
"sparks.createdAt": { $eq: new Date("2020-06-24T12:49:29.563+00:00") },
},
},
]);
res.status(200).json({ date: topUsers });
});
This is what I tried but did not work for me
I tried so many ways but faild
I was searching but not found anthing useful for my problem
You can do the following aggreagtion
User.aggregate([
{
'$project': {
'_id': 1,
'numberOfSparks': {
'$cond': {
'if': {
'$isArray': '$sparks'
},
'then': {
'$size': '$sparks'
},
'else': 'NA'
}
}
}
}, {
'$sort': {
'numberOfSparks': -1 //sort in descending order
}
}, {
'$limit': 10 // get top 10 users
}
])
Now this will work fine but if this aggregation is used frequently I would advice you a different approach. Aggregation is quite compute intensive it has to check all the users, then generate all the computed field, numberOfSparks, then sort. So as the number of users and sparks grow it could slow your server down.
Instead you could make a sparkCount field in your user schema. Then every time a spark is pushed to your User, you could increment a sparkCount field. Simply index the sparkCount field. Now you can easily query users with top sparkCount with a single query;
User.find({}).sort({sparkCount:-1}).limit(10)
This query will only read the 10 user documents with top sparkCount. No additional computation. And its also very scalable. Number of users and sparks will not effect performance.
Hope it helped

get count of conditionally matched elements from an array in MongoDB

I want comments with today's date and it should be non-empty and how much comments it has via using mongoose. I have tried a lot. Currently, I am trying to achieve with two methods. both have some problems let me explain. please consider I have only two posts in DB one has no comments like: [], and the other has 2 comments two inside it with today date and the 3 is old.
Method 1 :
in this method, it returns me today comment but it only returns single comment added on today.
and also returning me another object which has no comments
Post.find({ })
.select({
comments: { $elemMatch: { date: { $gt: startOfToday } } },
title: 1,
})
.exec((err, doc) => {
if (err) return res.status(400).send(err);
res.send(doc);
});
the output of above code is :
[{"_id":"5e9c67f0dd8479634ca255b1","title":"sdasd","comments":[]},{"_id":"5e9d90b4a7008d7bf0c4c96a","title":"sdsd","comments":[{"date":"2020-04-21T04:04:11.058Z","votes":
[{"user":"hhhh","vote":1}],"_id":"5e9e70bbece9c31b33f55041","author":"hhhh","body":"xvxgdggd"}]}]
Method 2 :
In this method I am using the same thing above inside the found object like this:
Post.find({ comments: { $elemMatch: { date: { $gt: startOfToday } } } })
.exec((err, doc) => {
if (err) return res.status(400).send(err);
res.send(doc);
});
And it returns me first post with all comments (3 comments) but not second post(that is good) that have empty comment array.
here is the output :
[{"author":{"id":"5e85b42f5e4cb472beedbebb","nickname":"hhhh"},"hidden":false,"_id":"5e9d90b4a7008d7bf0c4c96a","title":"sdsd","body":"dsfdsfdsf","votes":[{"user":"5e85b42f5e4cb472beedbebb","vote":1}],"comments":[{"date":"2020-04-20T12:08:32.585Z","votes":[],"_id":"5e9d90c0a7008d7bf0c4c96b","author":"hhhh","body":"zcxzczxc z zxc"},
{"date":"2020-04-21T04:04:11.058Z","votes":[{"user":"hhhh","vote":1}],"_id":"5e9e70bbece9c31b33f55041","author":"hhhh","body":"xvxgdggd"},
{"date":"2020-04-21T04:56:25.992Z","votes":[],"_id":"5e9e7cf96095882e11dc510c","author":"hhhh","body":"new should appear in feeds"}],"date":"2020-04-20T12:08:20.687Z","createdAt":"2020-04-20T12:08:20.692Z","updatedAt":"2020-04-21T04:56:26.003Z","__v":3}]
This is my post schema :
const postSchema = new Schema(
{
title: {
type: String,
required: true,
unique: 1,
index: true,
},
author: {
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
nickname: String,
},
body: {
type: String,
required: true,
},
comments: [
{
author: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
date: { type: Date, default: Date.now },
votes: [{ user: String, vote: Number, _id: false }],
},
],
date: { type: Date, default: Date.now },
hidden: {
type: Boolean,
default: false,
},
votes: [{ user: Schema.Types.ObjectId, vote: Number, _id: false }],
},
{ timestamps: true }
);
So, if I have SUM up the things I need today comments and today is 21st April (Two comments) and another comment date is 20. I only need today's comments with its count.
If I forgot something to add please let me know. Thanks
There are couple of changes as $elemMatch would return only the first matching element from array but not all the matching elements in comments array. So it's not useful here, additionally if you want comments for today you need to use $gte instead of $gt for input startOfToday. Finally, You need to use aggregation-pipeline to do this :
db.collection.aggregate([
/** Lessen the no.of docs for further stages by filter with condition */
{
$match: { "comments.date": { $gte: ISODate("2020-04-21T00:00:00.000Z") } }
},
/** Re-create `comments` array by using filter operator with condition to retain only matched elements */
{
$addFields: {
comments: {
$filter: {
input: "$comments",
cond: { $gte: ["$$this.date", ISODate("2020-04-21T00:00:00.000Z")] }
}
}
}
},
{
$addFields: { count: { $size: "$comments" } } // Add count field which is size of newly created `comments` array(Which has only matched elements)
}
]);
Test : mongoplayground

Push values into array with mongoose

I have a problem pushing values into an array with mongoose (yes I have read many topics about it and tried many ways to do it).
So I have this schema
const Postit = new Schema({
text: {
type: String,
required: true
},
status: {
type: String,
default: 'TODO'
},
modified: {
type: Date,
default: Date.now
},
user: {
type: ObjectId,
ref: 'User',
required: true
},
collaborators: [String]
})
And I'm trying to push a string in the collaborators property where the queries match.
So this is the method I use to update it
addCollaborator(uid, pid) {
return Postit.updateOne({
_id: pid,
user: uid
},
{ $push: { collaborators: 'pepe' } },
(err, raw) => {
//TO DO
})
}
But nothing happens. The query match because if I change $push for $set and put a new value to status property for example it updates.
The funny thing is that if I run it in mongodb client terminal it works.
db.postits.updateOne({
_id: ObjectId("5beb1492cf484233f8e21ac1"),
user: ObjectId("5beb1492cf484233f8e21abf")
},
{ $push: {collaborators: 'pepe' }
})
What i'm doing wrong?
Pick promises or callbacks but do not mix them together. Either you do:
addCollaborator(uid, pid) {
Postit.updateOne({
_id: mongoose.Types.ObjectId(pid),
user: uid
},
{ $push: { collaborators: 'pepe' } },
(err, raw) => {
// TO DO
})
}
Or you do:
addCollaborator(uid, pid) {
return Postit.updateOne({
_id: mongoose.Types.ObjectId(pid),
user: uid
},
{ $push: { collaborators: 'pepe' } }).exec()
.then(result => {
// TO DO
})
}
Also make sure your objectId is an actual mongoose.Types.ObjectId
Ok, the problem is I was using an old schema.
The code works perfectly. Enough code for today...

How to implement an upvote/downvote system in MongoDB/Mongoose?

I am new to MongoDB, and I'm trying to implement an upvote/downvote system so that users can vote on reviews in my application.
How I've set up my system is that the user sends an POST request via AJAX by pressing an upvote or downvote button in the application, which contains a boolean "upvote" which is an upvote if true, and a downvote if false (this code works so I didn't include it). Once the request reaches the server, the server checks if the review the user voted on contains a vote already. If not, the server adds a vote to the review's "votes" array and increments or decrements the voteBalance attribute of that review. If there already exists a vote in that review's "votes" array then it should either:
1) Modify it if the existing vote's upvote attribute is different from the new vote and then modify voteBalance accordingly, or
2) Delete the existing vote if its upvote attribute is the same as the new one and then modify voteBalance accordingly
My code for inserting a new vote works fine, but the issue I'm having is that I can't figure out how to make it work when a vote already exists. In the server-side code below, the else statement near the bottom is what I tried to handle case 1) from above, but it doesn't work. So how can I get both these cases to work?
Here is my Review schema:
var ReviewSchema = new mongoose.Schema({
authorID: {
type: String,
required: true,
},
movieID: {
type: Number,
required: true,
},
date: {
type: Number,
required: true
},
username: {
type: String,
required: true
},
score: {
type: Number,
required: true
},
text: {
type: String
},
voteBalance: {
type: Number,
required: true,
default: 0
},
votes: [{
voterID: {
type: String,
required: true,
},
upvote: {
type: Boolean,
required: true
}
}],
comments: [{
commenterID: {
type: String,
required: true,
},
text: {
type: String,
required: true
},
date: {
type: Number,
required: true
}
}]
},{collection: 'reviews'});
Here is the code I'm using to create and update votes on the server:
Review.findOne({_id: new ObjectID(reviewID), votes: { $elemMatch: { voterID: req.session._id }}}, function(err, review) {
if (err) {
console.log(err);
res.status(500).send();
}
//If vote was not found (or if review was not found), create vote
if (!review) {
if (upvote) {
var update = {$addToSet: {votes: {voterID: req.session._id, upvote}}, $inc : {voteBalance : 1}};
}
else {
var update = {$addToSet: {votes: {voterID: req.session._id, upvote}}, $inc : {voteBalance : -1}};
}
Review.findOneAndUpdate({_id: new ObjectID(reviewID)}, update, function(err, review) {
if (err) {
console.log(err);
return res.status(500).send();
}
res.status(200).send();
});
}
//If vote was found, update
else {
if (upvote) {
var update = {$set: { 'votes.$.upvote': upvote }, $inc : {voteBalance : 1}};
}
else {
var update = {$set: { 'votes.$.upvote': upvote }, $inc : {voteBalance : -1}};
}
Review.findOneAndUpdate({_id: new ObjectID(reviewID), 'votes.$.voterID': req.session._id}, update, function(err) {
if (err) {
console.log(err);
return res.status(500).send();
}
res.status(200).send();
});
}
});
Also, I recognize that this code is probably not as efficient as it could be, and I would appreciate any tips on that front as well.
Instead of doing findOne() and then findOneAndUpdate(), you would be better off using findOneAndUpdate() with the upsert option. That way you don't need that extra if statement in the callback.
I'd also recommend not storing votes as an array in the ReviewSchema. That array can grow without bound because any number of users can vote on a Review, which means a review document might become huge and unwieldy. I'd recommend using a mapping collection instead.

Resources