Mongoose aggregation query skipping getters - node.js

Im have the following mongoose schema
var schema = new mongoose.Schema({
subtotal: {type: Number, required: true, default: 0},
owner: { type : Number, required : true, index: true},
products: [{type: mongoose.Schema.ObjectId, ref: 'Product'}],
}, {
timestamps: true
});
schema.path('subtotal').set(function(p){
return p * 100;
});
schema.path('subtotal').get(function(p){
return parseFloat((p/100).toFixed(2));
});
schema.set('toJSON', {getters: true, setters:true});
schema.set('toObject', {getters: true, setters:true});
Running a simple find query returns the subtotal value as expected (e.g. 11.99).
When I run an aggregation query the subtotal value is not divided by 100 as I expect it to be.
Order.aggregate([
{ $match: { $and: [
{status: 'COMPLETE'},
{premises: mongoose.Types.ObjectId(premisesId)},
{createdAt:{
$gte: startOfToday,
$lt: endOfToday
}}
]}},
{
$group: {
_id: {hour: {$hour: "$createdAt"}},
count: { $sum: 1 },
sales: { $sum: '$subtotal'}
}
},
{
$sort: {
"_id.hour": 1
}
}
]).exec(function(err, orders){
//do something
})
Is this expected behaviour of aggregation?
Is there a way that I can force the aggregation query to fire the getters.

Short answer: yes, that is the expected behavior.
Longer answer requires referencing mongoose's documentation for Model.aggregate:
Arguments are not cast to the model's schema because $project operators allow redefining the "shape" of the documents at any stage of the pipeline, which may leave documents in an incompatible format.
Essentially this means the magic of anything mongoose allows via using a schema is not applied when using aggregates (including getters). You will need to include that transform in the pipeline.

Related

mongoose - Find by created time irrelevant of date

I have the following mongoose model
const Post = new Schema({
created_at: { type: Date, default: Date.now() },
updated_at: { type: Date },
postName: String,
postContent:String,
promoted: {
isPromoted: { type: Boolean, default: false, required: true },
promotedFrom: { type: Date },
promotedTill: { type: Date },
},
});
Example Document
const Post = new Schema({
created_at: 2020-07-05T16:16:38.139+00:00,
postName: My first Post,
postContent:This is my furst post,
promoted: {
isPromoted: true,
promotedFrom: 2020-11-13T16:14:38.139+00:00,
promotedTill: 2020-11-20T16:14:38.139+00:00,
},
});
With mongose I want to query the documents with the promotedTill time to be between 12.00 hrs to 16.30 hrs irrespective of any date.
Thanks in Advance.
Adjust the gt and lt to include/exclude boundaries:
db.foo.aggregate([
{$match: {$expr: {
$and: [
{$gt: [ {$hour: "$promoted.promotedTill"}, 12 ]},
{$lte: [{$hour: "$promoted.promotedTill"},16]},
{$lt: [{$minute: "$promoted.promotedTill"},30]}
]}
}}
]);
check this example
I've done the query quite simple, without other fields.
The query is this:
db.collection.find({
"$and": [
{
"promoted.promotedFrom": {
"$gte": ISODate("yourdate")
}
},
{
"promoted.promotedTill": {
"$lte": ISODate("yordate")
}
}
]
})
Basically is search a value within a range using $gte (greater than or equal) and $lte (lower than or equal).
Using the $and operator both conditions should be true.

Find last document of an array of documents with different values with Mongoose

I have an array of values that I use to query some data. I need to get the last document of each value in the array. I prefer to explain with some code:
Schema:
const quizResultSchema = new mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
answeredByUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
created: { type: Date, default: Date.now },
levelAnswered: { type: mongoose.Schema.Types.ObjectId, ref: 'QuizLevel' },
});
controller:
QuizResult.find(
{
levelAnswered: { $in: levelIds },
answeredByUser: result.applicant._id,
},
{},
{ sort: { created: -1 } }
)
levelIds is an array of Ids and I use it to return an array of documents. The problem is that I'm getting all the documents for each Id in the array sorted by date. What I need is to get the last created document and not all the documents for each Id.
How can I do that? Is it possible to do it just with Mongoose?
It's possible to do this by grouping and using $last like so:
db.collection.aggregate([
{
$match:{
levelAnswered: { $in: levelIds },
answeredByUser: result.applicant._id,
}
},
{
$group: {
_id: "$levelAnswered",
last: {$last: "$$ROOT"}
}
},
{
$replaceRoot: {
newRoot: "$last"
}
}
])

How can we sort data based on date in mongodb

how can I sort document in ascending or descending order in MongoDB
const contact = await AtHome.aggregate([
{
$project:{
name:1,
partnerName:1,
phone:1,
email:1,
city:1,
guest:1,
desc:1,
requestedOn: { $dateToString: { format: "%d-%m-%Y", date: "$createdAt" } },
weddingDate: { $dateToString: { format: "%d-%m-%Y", date: "$weddingDate" } },
}
},
{ $sort : { createdAt : -1 } }
]);
try {
res.send(contact);
} catch (err) {
res.send('INVALID');
}
I treid to add createdAt:1 to $project object but did not work.
AtHome.js (Schema)
const mongoose = require('mongoose');
const atHomeSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
partnerName: {
type: String,
required: true
},
email: {
type: String,
required: true
},
phone: {
type: String,
required: true
},
weddingDate: {
type: Date,
required: false
},
city: {
type: String,
required: true
},
guest: {
type: Number,
required: true
},
desc: {
type: String,
required: true
},
createdAt:{
type: Date,
default: Date.now()
}
});
module.exports = mongoose.model('AtHome', atHomeSchema);
I added AtHome.js which is my schema file
This is what I tried but not working, I am just getting the unsorted document
Please help me
Thank You
As per the Aggregation framework, you have to first sort the data and then project it to desired output. Try the following query:
const contact = await AtHome.aggregate([
{ $sort : { createdAt : -1 } },
{ $project: {
name:1,
partnerName:1,
phone:1,
email:1,
city:1,
guest:1,
desc:1,
requestedOn: { $dateToString: { format: "%d-%m-%Y", date: "$createdAt" } },
weddingDate: { $dateToString: { format: "%d-%m-%Y", date: "$weddingDate" } },
}
}
]);
try {
res.send(contact);
} catch (err) {
res.send('INVALID');
}
As mentioned in the above answers you need to sort the data before project. I can see the in you Schema that you are using default: Date.now() for the createdAt, this will set the value of createdAt to time when Schema is defined, So all the records will have same value for createdAt until next restart of the server. You can use Date.now instead, if you need to set the createdAt to time when the record is created
createdAt:{
type: Date,
default: Date.now
}
You have to include the createdAt field in projection or change the order of pipeline stages $sort then $project.
Each stage transforms the documents as they pass through the pipeline. In your first stage, you have filtered out createdAt so the second stage doesn't take any effect. It is recommended to use $sort before $project stage so $sort stage may use index. This will provide better performance.
// sort order 1 -> asc, -1 ->desc
await AtHome.aggregate([ {$sort : { createdAt : 1 }} ,{$project : {...}}])
I think your first step should be to understand how aggregation pipeline works. In pipeline stages, the data goes from one stage to another.On each stage we do some modifications in data and that modified data is passed on to next pipeline stage.
Here your first stage is project which return the fields you specified:
and this data is then passed on to sorting stage and as the sort stage can not find createdAt field in data from first stage, so unsorted data is returned.
So add
const contact = await AtHome.aggregate([
{
$project:{
name:1,
partnerName:1,
phone:1,
email:1,
city:1,
guest:1,
desc:1,
requestedOn: { $dateToString: { format: "%d-%m-%Y", date: "$createdAt" } },
weddingDate: { $dateToString: { format: "%d-%m-%Y", date: "$weddingDate" } },
createdAt : 1
}
},
{ $sort : { createdAt : -1 } }
]);
which gives createdAt field which sort stage can use:
Using sort as a first stage in pipeline is very bad idea. If millions of records are there then the query will be very slow. So it always good to use sort before the limit stage but after the match stage(so that we have less number of docs to sort).
Use timestamp option for createdAt and updatedAt fields in documents.
const schema = Schema({
//your fields
}, {
timestamps: true }
});
Read More : https://mongoosejs.com/docs/guide.html#timestamps

How to make aggregate + populate in Mongoose

I appreciate some help. I'm doing an api rest with express and mongodb (v3.4.4), using mongoose (v4.10.5). I need to do an aggregation operation, but I do not deal with it. I show you some code. The models (it has more properties, but I have left it simple):
const CategoryModel = mongoose.model('Category', new Schema({
slug: { type: String, unique: true, lowercase: true, index: true },
description: String
}));
const MyModel = mongoose.model('MyModel', new Schema({
category: { type: Schema.Types.ObjectId, ref: 'Category' },
other: [{ type: Schema.Types.ObjectId, ref: 'Other' }],
times_count: { type: Number, default: 0 }
}));
Important, I'm interested in populate category field of MyModel, not other field.
Suppose Category and MyModel has certain records well formed. The request:
MyModel.aggregate([
{
$group : {
_id : '$_id',
times: { $sum: '$times_count' }
}
},
{
$limit: 5
}
]).limit(5).exec().then((data) => {
console.log(data);
}).catch((err) => {
console.error(err);
});
data is correct, has 5 records, but not include category. Now, I try with:
MyModel.aggregate([
{
$group : {
_id : '$_id',
times: { $sum: '$times_count' }
}
},
{
$limit: 5
},
{
$lookup: {
from: 'Category', // I tried with 'Categories' and 'categories'
localField: 'category',
foreignField: '_id',
as: 'category'
}
},
{
$unwind: '$category'
}
]).limit(5).exec().then((data) => {
console.log(data);
}).catch((err) => {
console.error(err);
});
Now data is empty. I set mongoose.set('debug', true); and the operations they look right, inclusive the last operation aggregate, but data is empty...
I do not know if I explained well. Obviously there is something that I am not fully understanding. Thanks in advance.
I get the desired records in objs, the problem is that I only come with the _id and times properties, and I need to populate the category.
That's about right since you didn't explicitedly add the stage to join the other collection.
I've tried adding $project to the aggregation after the $ group but nothing.
In simple terms, $project is for including and excluding new fields using one collection, not joining.
You are looking for $lookup which is for joining one collection with another. When you join a new collection, each document will have a new array field containing the "joined" documents from the other collection.
In your case, your new array field will have one document from the other collection, so you probably want to $unwind also.
MyModel.aggregate([
{
$group : {
_id : '$_id',
times: { $sum: '$times_count' },
category: { $first: '$category' }
}
},
/*
{
$limit: 5
},
*/
{
$lookup: {
from: 'Categories',
localField: 'category',
foreignField: '_id',
as: 'category'
}
},
{
$unwind: '$category'
}
]).exec(...);
In terms of your initial problem, try uncommenting the 2nd stage above and not using limit(5) in your first example.

Add elements in nested document then retrieve the _id

I have the following collection definition:
// Includes
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
// Create required sub schemas
const subSchema0 = new Schema({
value: String,
});
const subSchema = new Schema({
idWordsLibraryName: {
type: Schema.Types.ObjectId,
ref: 'WordsLibrary1_0',
},
type: String,
values: [
subSchema0,
],
});
const schema = new Schema({
version_: String,
idWordsLibraryName: {
type: Schema.Types.ObjectId,
ref: 'WordsLibrary1_0',
},
idsDads: [{
type: Schema.Types.ObjectId,
ref: 'LocationStructure1_0',
}],
params: [
subSchema,
],
});
Summary -> One document with nested parameters with nested values.
I have the following request that add some values into a particular parameter
this.findOneAndUpdate({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
$push: {
'params.$.values': {
$each: dataToPush,
},
},
}, {
new: true,
});
It works as expected.
What I want now is to get the _id of pushed elements, but without loading all values of the parameter.
I have tried to use the select option of findOneAndUpdate but it don't work using the projection:
this.findOneAndUpdate({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
$push: {
'params.$.values': {
$each: dataToPush,
},
},
}, {
new: true,
select: {
'params.$.values': 1,
},
});
It gets me:
{
"_id": "57273904135f829c3b0739dd",
"params": [
{},
{},
{},
{},
],
},
I have tried to perform a second request to get the _ids as well, but it don't work either:
this.find({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
_id: 1,
'params.$.values': {
$slice: -nbAdded,
},
});
If you have any idea of how retrieving the _id of the pushed values without loading all values of the parameter, you are very welcome :)
Well after tons of researches all over the web and stack overflow <3 I have found a solution, which is:
this.aggregate([{
$match: {
_id: new mongoose.Types.ObjectId(data.idLocationStructure),
},
},
{
$unwind: '$params',
}, {
$match: {
'params._id': new mongoose.Types.ObjectId(data.idLocationStructureParameter),
},
},
{
$unwind: '$params.values',
},
{
$sort: {
'params.values._id': -1
},
},
{
$limit: nbAdded,
},
{
$project: {
_id: '$params.values._id',
},
},
]);
If you experience the same problem, here is the explaination:
$match makes me taking the good high level document
$unwind makes me to go into the params array in the document we $match
$match makes me taking the good parameter
$unwind makes me to go into the values array
I $sort all values by _id DESC
I $limit to the number of values I added previsoulsy
I change the name of the _id (like an alias)
So I got as result an array that contains the last added values _ids

Resources