mongoose aggregate sort does not work properly - node.js

the $sort method in this query does not work properly, I want to list aggregations and they should be listed depending on $sort query I defined
const newConverSation = await Messenger.Messenger.aggregate([
{
$sort: { 'createdAt': -1 }
},
{ $match: {
users: mongoose.Types.ObjectId(req.body.userid)}},
{
$group: {
_id: { $setUnion: "$users" },
message: { $first: "$$ROOT" },
},
},
])
It sometimes sort correctly, sometimes does not, randomly...
--------------------------------edited for another question
this is my toJSON method which normally works properly
messengerScheme.methods.toJSON = function(){
const messenger = this
const messengerObject = messenger.toObject()
messengerObject.createdAt = moment(messengerObject.createdAt).fromNow()
return messengerObject
}
when I query my model directly I get createdAt : "7 hours", However when I query it with such an aggregation I get createdAt : "2021-05-17T11:34:47.475Z", so toJSON method does not work in my aggregation

Related

aggregate with dynamic field path- mongo DB and nodeJS

I have a collection- 'products' that contains the following documents:
{
productName: "computer",
updateAt: "2022-07-12T12:44:47.485Z",
createAt: ""2022-06-12T10:34:03.485Z",
changeAt: ""2022-09-12T10:39:40.485Z"
}
I want to create an aggregation that convert the field "updateAt" from string to date.
for this, I created this aggregation:
db.products.aggregate([{
$set: {
updateAt: {
$dateFromString: {
dateString: '$updateAt'
}
}
},
},
{
$out: 'products'
}]
)
It works fine for this need, but as you can see I specified the field path "updateAt" in a hard coded way.I want to use the above aggregation in a dynamic way-
considering I have an array of fields that I want to change:
const fields = ['updateAt', 'createAt', 'changeAt']
I want to loop over the fields array and use each field as a fieldPath so I can transfer the field name to the aggregation, something like that-
fields.forEech(field -> {
db.products.aggregate([{
$set: {
`${field}`: {
$dateFromString: {
dateString: `$${field}`
}
}
},
},
{
$out: 'products'
}]
)
}
As you can understand it's not working for me....
How can I achieve my goal?
You have some errors in your nodejs function, also, aggregate method returns a Promise, so you will need to wait for it, to resolve before moving further.
Try this:
const fields = ['updateAt', 'createAt', 'changeAt']
for(let i=0; i < fields.length; i++) {
let field = fields[i];
await db.products.aggregate([{
"$set": {
[field]: {
"$dateFromString": {
"dateString": `$${field}`
}
}
},
},
{
"$out": 'products'
}]
)
}
Also, make the function containing this piece of code async.

How to sum all amount paid by users

I am finding it defficult to add up all amount paid by customers that ordered items
Order Schema
const orderschema = new Mongoose.Schema({
created: { type: Date, default: Date.now() },
amount: { type: Number, default: 0 }
User: [{ type: Mongoose.Schema.ObjectId, ref: 'Users'}]
...
})
Route
Get('/total-amount', total-amount)
Controller
Exports.total-amount = () => {
Order.find()...
}
I don't know what to add here to get the total amount made by all customers.
Using NodeJS and MongoDB.
Thank you for you help
You can use $sum in an aggregation stage like this:
First $group all (without _id is to group all values)
Then create field total which is the sum of al amount.
And an optional stage, $project to output only total field.
db.order.aggregate({
"$group": {
"_id": null,
"total": {
"$sum": "$amount"
}
}
},
{
"$project": {
"_id": 0
}
})
Example here
To add into a controller using nodeJS and Mongoose you can use something like this piece of code:
Exports.total - amount = (req, res) => {
Order.aggregate({
"$group": {
"_id": null,
"total": {
"$sum": "$amount"
}
}
}, {
"$project": {
"_id": 0
}
}).then(response => {
res.status(200).send(response)
}).catch(e => res.status(400).send())
}
Note hoy the operation is done using your mongoose model (in this case Order). You are calling aggregate method in the same way you call find method for example: Instead of doing
yourModel.find()
Is
yourModel.aggregate()
And the response will be:
[
{
"total": 6
}
]
So even you can update your controller to add a if/else block like this:
if(response[0].total)
res.status(200).send(response[0].total)
else
res.status(404).send()

mongoose schema transform not invoked if document is returned directly from query

I have an endpoint that does an operation such as this:
const pipeline = [
{
$match: {
$and: [
{
$or: [...],
},
],
},
},
{
$group: {
_id : '$someProp',
anotherProp: { $push: '$$ROOT' },
},
},
{ $sort: { date: -1 } },
{ $limit: 10 },
]
const groupedDocs = await MyModel.aggregate(pipeline);
The idea here is that the returned documents look like this:
[
{
_id: 'some value',
anotherProp: [ /* ... array of documents where "someProp" === "some value" */ ],
},
{
_id: 'another value',
anotherProp: [ /* ... array of documents where "someProp" === "another value" */ ],
},
...
]
After getting these results, the endpoint responds with an array containing all the members of anotherProp, like this:
const response = groupedDocs.reduce((docs, group) => docs.concat(group.anotherProp), []);
res.status(200).json(response);
My problem is that the final documents in the response contain the _id field, but I want to rename that field to id. This question addresses this issue, and specifically this answer is what should work, but for some reason the transform function doesn't get invoked. To put it differently, I've tried doing this:
schema.set('toJSON', {
virtuals: true,
transform: function (doc, ret) {
console.log(`transforming toJSON for document ${doc._id}`);
delete ret._id;
},
});
schema.set('toObject', {
virtuals: true,
transform: function (doc, ret) {
console.log(`transforming toObject for document ${doc._id}`);
delete ret._id;
},
});
But the console.log statements are not executed, meaning that the transform function is not getting invoked. So I still get the _id in the response instead of id.
So my question is how can I get id instead of _id in this scenario?
Worth mentioning that toJSON and toObject are invoked (the console.logs show) in other places where I read properties from the documents. Like if I do:
const doc = await MyModel.findById('someId');
const name = doc.name;
res.status(200).json(doc);
The response contains id instead of _id. It's almost like the transform function is invoked once I do anything with the documents, but if I pass the documents directly as they arrive from the database, neither toJSON nor toObject is invoked.
Thanks in advance for your insights. :)
The toJSON and toObject methods won't work here because they don't apply to documents from an aggregation pipeline. Mongoose doesn't convert aggregation docs to mongoose docs, it returns the raw objects returned by the pipeline operation. I ultimately achieved this by adding pipeline stages to first add an id field with the same value as the _id field, then a second stage to remove the _id field. So essentially my pipeline became:
const pipeline = [
{
$match: {
$and: [
{
$or: [...],
},
],
},
},
// change the "_id" to "id"
{ $addFields: { id: '$_id' } },
{ $unset: ['_id'] },
{
$group: {
_id : '$someProp',
anotherProp: { $push: '$$ROOT' },
},
},
{ $sort: { date: -1 } },
{ $limit: 10 },
]
const groupedDocs = await MyModel.aggregate(pipeline);
It is possible to recast the raw objects into mongoose documents after getting them from the aggregate. You just need to transform them back one by one. They will then trigger the toJSON on return.
const document = Model.hydrate(rawObject);
Answer found here:
Cast plain object to mongoose document

Mongoose sort the aggregated result

I'm having a lot of difficulty in solving this mongodb (mongoose) problem.
There is this schema 'Recommend' (username, roomId, ll and date) and its collection contains recommendation of user.
I need to get a list of most recommended rooms (by roomId). Below is the schema and my tried solution with mongoose query.
var recommendSchema = mongoose.Schema({
username: String,
roomId: String,
ll: { type: { type: String }, coordinates: [ ] },
date: Date
})
recommendSchema.index({ ll: '2dsphere' });
var Recommend = mongoose.model('Recommend', recommendSchema);
Recommend.aggregate(
{
$group:
{
_id: '$roomId',
recommendCount: { $sum: 1 }
}
},
function (err, res) {
if (err) return handleError(err);
var resultSet = res.sort({'recommendCount': 'desc'});
}
);
The results returned from the aggregation pipeline are just plain objects. So you do the sorting as a pipeline stage, not as a separate operation:
Recommend.aggregate(
[
// Grouping pipeline
{ "$group": {
"_id": '$roomId',
"recommendCount": { "$sum": 1 }
}},
// Sorting pipeline
{ "$sort": { "recommendCount": -1 } },
// Optionally limit results
{ "$limit": 5 }
],
function(err,result) {
// Result is an array of documents
}
);
So there are various pipeline operators that can be used to $group or $sort or $limit and other things as well. These can be presented in any order, and as many times as required. Just understanding that one "pipeline" stage flows results into the next to act on.

MongoDB aggregate query with a where in node.js

I have the following mongodb query in node.js which gives me a list of unique zip codes with a count of how many times the zip code appears in the database.
collection.aggregate( [
{
$group: {
_id: "$Location.Zip",
count: { $sum: 1 }
}
},
{ $sort: { _id: 1 } },
{ $match: { count: { $gt: 1 } } }
], function ( lookupErr, lookupData ) {
if (lookupErr) {
res.send(lookupErr);
return;
}
res.send(lookupData.sort());
});
});
How can this query be modified to return one specific zip code? I've tried the condition clause but have not been able to get it to work.
Aggregations that require filtered results can be done with the $match operator. Without tweaking what you already have, I would suggest just sticking in a $match for the zip code you want returned at the top of the aggregation list.
collection.aggregate( [
{
$match: {
zip: 47421
}
},
{
$group: {
...
This example will result in every aggregation operation after the $match working on only the data set that is returned by the $match of the zip key to the value 47421.
in the $match pipeline operator add
{ $match: { count: { $gt: 1 },
_id : "10002" //replace 10002 with the zip code you want
}}
As a side note, you should put the $match operator first and in general as high in the aggregation chain as you can.

Resources