Mongoose how to write a query with if condition? - node.js

Suppose I have the following query:
post.getSpecificDateRangeJobs = function(queryData, callback) {
var matchCriteria = queryData.matchCriteria;
var currentDate = new Date();
var match = { expireDate: { $gte: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()) } };
if (queryData.matchCriteria !== "") {
match = {
expireDate: { $gte: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()) },
$text: { $search: matchCriteria }
};
}
var pipeline = [
{
$match: match
},
{
$group: {
_id: null,
thirtyHourAgo: {
$sum: {
$cond: [
{
$gte: [
"$publishDate",
new Date(queryData.dateGroups.thirtyHourAgo)
]
},
1,
0
]
}
},
fourtyEightHourAgo: {
$sum: {
$cond: [
{
$gte: [
"$publishDate",
new Date(queryData.dateGroups.fourtyHourAgo)
]
},
1,
0
]
}
},
thirtyDaysAgo: {
$sum: {
$cond: [
{
$lte: [
"$publishDate",
new Date(queryData.dateGroups.oneMonthAgo)
]
},
1,
0
]
}
}
}
}
];
var postsCollection = post.getDataSource().connector.collection(
post.modelName
);
postsCollection.aggregate(pipeline, function(err, groupByRecords) {
if (err) {
return callback("err");
}
return callback(null, groupByRecords);
});
};
What i want to do is:
1- check if queryData.dateGroups.thirtyHourAgo existed and has value, then only add the relevant match clause in query (count of posts only for past 30 hour).
2- check if queryData.dateGroups.fourtyHourAgo existed, then add relevant query section (count of posts for past 30 hour, and past 48 hour ago).
3 and the same for queryData.dateGroups.oneMonthAgo (count of posts for past 30 hour, 48 hour, and past one month).
I need something like Mysql if condition to check if a variable existed and not empty then include a query clause. Is it possible to do that?
My sample data is like:
/* 1 */
{
"_id" : ObjectId("58d8bcf01caf4ebddb842855"),
"vacancyNumber" : "123213",
"position" : "dsfdasf",
"number" : 3,
"isPublished" : true,
"publishDate" : ISODate("2017-03-11T00:00:00.000Z"),
"expireDate" : ISODate("2017-05-10T00:00:00.000Z"),
"keywords" : [
"dasfdsaf",
"afdas",
"fdasf",
"dafd"
],
"deleted" : false
}
/* 2 */
{
"_id" : ObjectId("58e87ed516b51f33ded59eb3"),
"vacancyNumber" : "213123",
"position" : "Software Developer",
"number" : 4,
"isPublished" : true,
"publishDate" : ISODate("2017-04-14T00:00:00.000Z"),
"expireDate" : ISODate("2017-05-09T00:00:00.000Z"),
"keywords" : [
"adfsadf",
"dasfdsaf"
],
"deleted" : false
}
/* 3 */
{
"_id" : ObjectId("58eb5b01c21fbad780bc74b6"),
"vacancyNumber" : "2432432",
"position" : "Web Designer",
"number" : 4,
"isPublished" : true,
"publishDate" : ISODate("2017-04-09T00:00:00.000Z"),
"expireDate" : ISODate("2017-05-12T00:00:00.000Z"),
"keywords" : [
"adsaf",
"das",
"fdafdas",
"fdas"
],
"deleted" : false
}
/* 4 */
{
"_id" : ObjectId("590f04fbf97a5803636ec66b"),
"vacancyNumber" : "4354",
"position" : "Software Developer",
"number" : 5,
"isPublished" : true,
"publishDate" : ISODate("2017-05-19T00:00:00.000Z"),
"expireDate" : ISODate("2017-05-27T00:00:00.000Z"),
"keywords" : [
"PHP",
"MySql"
],
"deleted" : false
}
Suppose I have three link in my application interface:
1- 30 hour ago posts.
2- 48 hour ago posts.
3- last one month posts.
Now if user click on first link i should control to group posts only for 30 hour ago, but if user click on second link, i should prepare my query to group posts for 30 hour and also for 48 hour, and if user click on third link i should prepare for all of them.
I want something like:
var pipeline = [
{
$match: match
},
{
$group: {
_id: null,
if (myVariable) {
thirtyHourAgo: {
........
........
}
}
if (mysecondVariable) {
fortyEightHourAgo: {
........
........
}
}

You can use javascript to dynamically create json document based on your query parameters.
Your updated function will look something like
post.getSpecificDateRangeJobs = function(queryData, callback) {
var matchCriteria = queryData.matchCriteria;
var currentDate = new Date();
// match document
var match = {
"expireDate": {
"$gte": currentDate
}
};
if (matchCriteria !== "") {
match["$text"]: {
"$search": matchCriteria
}
};
// group document
var group = {
_id: null
};
// Logic to calculate hours difference between current date and publish date is less than 30 hours.
if (queryData.dateGroups.thirtyHourAgo) {
group["thirtyHourAgo"] = {
"$sum": {
"$cond": [{
"$lte": [{
"$divide": [{
"$subtract": [currentDate, "$publishDate"]
}, 1000 * 60 * 60]
}, 30]
},
1,
0
]
}
};
}
// Similarly add more grouping condition based on query params.
var postsCollection = post.getDataSource().connector.collection(
post.modelName
);
// Use aggregate builder to create aggregation pipeline.
postsCollection.aggregate()
.match(match)
.group(group)
.exec(function(err, groupByRecords) {
if (err) {
return callback("err");
}
return callback(null, groupByRecords);
});
};

As I understood, I can suggest you following general query. Modify this according to your need.
db.getCollection('vacancy')
.aggregate([{$match: { $and: [
{publishDate:{ $gte: new Date(2017, 4, 13) }} ,
{publishDate:{ $lte: new Date(2017, 4, 14) }}
]} }])
Summary:
Used match to filter out result.
We are using aggregation Pipeline so you can add more aggregate operators n the pipeline
Using $and perform a logical AND because we want to fetch some documents between a give range say 1 day, 2 days or 1 month (change date parameters according to your requirement)

Related

Mongo aggregation $count pipeline to return count and objects [duplicate]

I want to perform an aggregation query that does basic pagination:
Find all orders that belongs to a certain company_id
Sort the orders by order_number
Count the total number of documents
Skips to e.g. document number 100 and passes on the rest
Limits the number of documents to e.g. 2 and passes them on
Finishes by returning the count and a selected few fields from the documents
Here is a breakdown of the query:
db.Order.collection.aggregate([
This finds all matching documents:
{ '$match' : { "company_id" : ObjectId("54c0...") } },
This sorts the documents:
{ '$sort' : { 'order_number' : -1 } },
This counts the documents and passes the unmodified documents, but I'm sure doing it wrong, because things turn weird from here:
{
'$group' : {
'_id' : null,
'count' : { '$sum' : 1 },
'entries' : { '$push' : "$$ROOT" }
}
},
This seems to skip some documents:
{ "$skip" : 100 },
This is supposed to limit the documents, but it does not:
{ "$limit" : 2 },
This does return the count, but it does not return the documents in an array, instead it returns arrays with each field:
{ '$project' : {
'count' : 1,
'entries' : {'_id' : "$entries._id", 'order_number' : "$entries.order_number"}
}
}
])
This is the result:
[
{ "_id" : null,
"count" : 300,
"entries" : [
{
"_id" : [ObjectId('5a5c...'), ObjectId('5a5c...')],
"order_number" : ["4346", "4345"]
},
{
"_id" : [ObjectId('5a5c...'), ObjectId('5a5c...')],
"order_number" : ["4346", "4345"]
},
...
]
}
]
Where do I get it wrong?
To calculate totals and return a subset, you need to apply grouping and skip/limit to the same dataset. For that you can utilise facets
For example to show 3rd page, 10 documents per page:
db.Order.aggregate([
{ '$match' : { "company_id" : ObjectId("54c0...") } },
{ '$sort' : { 'order_number' : -1 } },
{ '$facet' : {
metadata: [ { $count: "total" }, { $addFields: { page: NumberInt(3) } } ],
data: [ { $skip: 20 }, { $limit: 10 } ] // add projection here wish you re-shape the docs
} }
] )
It will return a single document with 2 fields:
{
"metadata" : [
{
"total" : 300,
"page" : 3
}
],
"data" : [
{
... original document ...
},
{
... another document ...
},
{
... etc up to 10 docs ...
}
]
}
Since mongoDB version 5.0 there is another option, that allows to avoid the disadvantage of $facet, the grouping of all returned document into a one big document. The main concern is that a document as a size limit of 16M. Using $setWindowFields allows to avoid this concern:
db.Order.aggregate([
{$match: {company_id: ObjectId("54c0...") } },
{$sort: {order_number: -1 } },
{$setWindowFields: {output: {totalCount: {$count: {}}}}}
{$skip: 20 },
{$limit: 10 }
])

Count Documents in mongoose in single query

const postsCount = await Post.find(query).countDocuments();
const allPosts = await Post.find(query)
.populate('likes')
.skip(startIndex)
.limit(noOfRecordsPerPage)
.sort({ createdAt: 'desc' });
I am querying two times in Database to find the total document count . How can i do this with a single query to the database ?
//you can use aggregation pipeline and $facet to combine two DB requests into one
//Actual Code output from windows Mongoshell/Command Line Interface(CLI)
> print("mongoDB",db.version())
mongoDB 4.2.6
> db.groups1.aggregate([
... {$facet:{
... "docCount":[
... {$group:{
... _id:null,
... count:{$sum:1}
... }
... }
... ],
... "allDocs":[
... {$match:{}}
... ]
... }
... }
... ]).pretty();
{
"docCount" : [
{
"_id" : null,
"count" : 4
}
],
"allDocs" : [
{
"_id" : ObjectId("5f817d142689512ef0bd3bd7"),
"uid" : 1,
"group_id" : "g1"
},
{
"_id" : ObjectId("5f817d142689512ef0bd3bd8"),
"uid" : 1,
"group_id" : "g2"
},
{
"_id" : ObjectId("5f817d142689512ef0bd3bd9"),
"uid" : 2,
"group_id" : "g2"
},
{
"_id" : ObjectId("5f817d142689512ef0bd3bd1"),
"uid" : 2,
"group_id" : "g3"
}
]
}
>
Use the $facet aggregation stage to combine multiple nested pipelines into your master pipeline.
Here's a working code of the above idea -
const countPipeline = [
{
$match: query
},
{
$group: {
_id: null,
count: {
$sum: 1
}
}
},
{
$project: {
_id: 0,
count: '$count'
}
}
];
const postPipeline = [
{
$match: query
},
{
$skip: startIndex
},
{
$limit: noOfRecordsPerPage
},
{
$sort: {
createdAt: -1
}
}
];
const pipeline = [
{
$facet: {
posts: postPipeline,
count: countPipeline
}
}
];
const results = await Posts.aggreagate(pipeline).exec();

How to use comparison in Mongodb filter

Context: I am making a website in which users can create accounts. Then they have to verify their accounts by opening the verification email. I am using Node.js and Mongodb.
What I'm Trying To Do: If the user has not verified their account in the past 24 hours, then delete their account.
Example account to delete:
{
created: 3294038434,
notValid: "dslafjksdfkj"
}
Example account not to delete:
{
created: 203498324,
notValid: false
}
The created key stores a date as a number.
If notValid is false, then the account is verified. If notValid is a string, then the account is yet to be verified and the string represents the verification code.
Is there a way to user deleteMany() and have a filter something like this?
{
Date.now() > created + 1000 * 60 * 60 * 24 && typeof(notValid) == "string"
}
I know that I can just read every single user and then do the logic but is there a way to just have mongodb do the logic / filter for me?
Working from mongo shell, lets take these three input documents:
{ "_id" : 2, "created" : 1574831443519, "notValid" : "dslafjksdfkj-abc" }
{ "_id" : 3, "created" : 1574817043519, "notValid" : true }
{ "_id" : 1, "created" : 1574817043519, "notValid" : "abc-111" }
Create a filter for the query to select documents with the criteria:
Date.now() > created + 1000 * 60 * 60 * 24 && typeof(notValid) ==
"string"
var queryFilter = { $cond: {
if: { $and: [
{ $eq: [ { $type: "$notValid"}, "string" ] },
{ $gt: [ new Date(), { $toDate: { $add: [ "$created", 86400000 ] } } ] }
]
},
then: true,
else: false
}
};
The query db.colln.find( { $expr: { $eq: [ queryFilter, true ] } } ) returns the document:
{ "_id" : 1, "created" : 1574817043519, "notValid" : "abc-111" }
Applying the query filter to delete the matching documents:
db.colln.deleteMany( { $expr: { $eq: [ queryFilter, true ] } } );
Deletes the document with _id : 1.
Note the created field has date/time as milliseconds. At the time I tested, the date/time and corresponding millis: ISODate("2019-11-28T03:20:03.835Z") and 1574911160308.

MongoDB Aggregate function for returning day wise count for a particular date range

I need to get the count of individual users for a particular date range that too on each day basis. Let's say, there are a total of 100 users within a month (1st - 30th), I need to get the count like
{
1st - 2 users
2nd - 10 users
}
MessagesSchema.statics.totalMessagesGraph = (id, startDate, endDate, platform) => {
return Messages.aggregate([
{
$match: {
id: id,
platform: platform,
timestamp: {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
}
])
}
What should be here to get the desired result ?
Expected Result:
For that particular date ranges the count for each day.
{
date1 - 20,
date2 - 22,
date3 - 24,
...
date30 - 12
}
The expected output should look like above. What query should be proceeded after $match. If possible please take a sample dataset and provide the output.
Use $group to get day wise count
for example
db.collection.aggregate([
{
$match: {
//id: id,
//platform: platform,
//timestamp: {
//$gte: new Date(startDate),
//$lte: new Date(endDate)
//}
//}
// Your matching logic
},
/* Now grouping users based on _id or id parameter for each day
from the above match results.
$createdAt can be replaced by date property present in your database.
*/
{ $group : {
id : { day: { $dayOfMonth: "$createdAt" },
month: { $month: "$createdAt" },
year: { $year: "$createdAt" } },
count : {$sum : 1}
}
}
])
Based on this you will get output like :
{
"_id" : {
"day" : 14,
"month" : 1,
"year" : 2017
},
"count" : 2.0
}
/* 2 */
{
"_id" : {
"day" : 31,
"month" : 1,
"year" : 2017
},
"count" : 8.0
}
/* 3 */
{
"_id" : {
"day" : 2,
"month" : 1,
"year" : 2017
},
"count" : 4.0
}
...
You can use the above query results to get required output.
More precisely you can remove month and year parameters from group query to get output like :
/* 1 */
{
"_id" : {
"day" : 25
},
"count" : 7.0
}
/* 2 */
{
"_id" : {
"day" : 18
},
"count" : 4.0
}
/* 3 */
{
"_id" : {
"day" : 17
},
"count" : 4.0
}
...
For reference you can check the mongoDB documentation also refer this.
MongoDB Aggregation Queries for "Counts Per Day"
Hope above example help you in getting the required output.
Here is the solution which I figured out after few trials.
{
'$project': {
timestamp: {'$dateToString': {format: '%Y-%m-%d', date: '$timestamp'}} }
}, {
'$group': {
_id: {timestamp: '$timestamp'},
count: {'$sum': 1}
}
}
And here is the output
"response": [
{
"_id": {
"timestamp": "2019-01-08"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-13"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-16"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-17"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-01-19"
},
"count": 1
},
{
"_id": {
"timestamp": "2019-02-01"
},
"count": 1
}
]

How do I modify the output of a Mongoose query?

I have a field in my MongoDB collection products called date_expired.
It's of type: date and stores the a date string.
I want to retrieve all the products and change the date_expired property in the result to number of hours left from now. How do I do this?
It's similar to getter() in Laravel...?
You could create a virtual property that will return the number of hours until expiry:
ProductSchema.virtual('hoursToExpiry').get(function() {
return (this.date_expired - Date.now()) / 3600000;
});
To access this property:
console.log('hours to expiry:', doc.hoursToExpiry)
If you want to include that property in any JSON or JS object, make sure that you set virtuals : true:
console.log('%j', doc.toJSON({ virtuals : true }));
Would consider using the aggregation framework in this case to output the transformation. You can use the $project pipeline arithmetic operators $divide and $subtract to achieve the final goal. These will enable you to carry out the arithmetic of calculating the number of hours to expiry i.e. implement the formula:
hoursToExpiry = (date_expired - timeNow)/1000*60*60 //the time units are all in milliseconds
Take for instance the following short mongo shell demo that will strive to drive home this concept:
Populate test collection:
db.test.insert([
{
"date_expired": ISODate("2016-03-27T10:55:13.069Z"),
"name": "foo"
},
{
"date_expired": ISODate("2016-06-11T20:55:13.069Z"),
"name": "bar"
},
{
"date_expired": ISODate("2016-06-11T16:17:23.069Z"),
"name": "buzz"
}
])
Aggregation Operation:
db.test.aggregate([
{
"$project": {
"name": 1,
"dateExpired": "$date_expired",
"dateNow": { "$literal": new Date() },
"hoursToExpiry": {
"$divide": [
{ "$subtract": [ "$date_expired", new Date() ] },
1000*60*60
]
}
}
}
])
Result (at the time of writing):
{
"result" : [
{
"_id" : ObjectId("575c0f6e8101b29fc93e5b9d"),
"name" : "foo",
"dateExpired" : ISODate("2016-03-27T10:55:13.069Z"),
"dateNow" : ISODate("2016-06-11T13:36:21.025Z"),
"hoursToExpiry" : -1826.685543333333
},
{
"_id" : ObjectId("575c0f6e8101b29fc93e5b9e"),
"name" : "bar",
"dateExpired" : ISODate("2016-06-11T20:55:13.069Z"),
"dateNow" : ISODate("2016-06-11T13:36:21.025Z"),
"hoursToExpiry" : 7.314456666666667
},
{
"_id" : ObjectId("575c0f6e8101b29fc93e5b9f"),
"name" : "buzz",
"dateExpired" : ISODate("2016-06-11T16:17:23.069Z"),
"dateNow" : ISODate("2016-06-11T13:36:21.025Z"),
"hoursToExpiry" : 2.683901111111111
}
],
"ok" : 1
}
With the above pipeline, you can then adopt it to your Mongoose implementation with the aggregate() method as basis of your query:
Product.aggregate([
{
"$project": {
"name": 1,
"dateExpired": "$date_expired",
"dateNow": { "$literal": new Date() },
"hoursToExpiry": {
"$divide": [
{ "$subtract": [ "$date_expired", new Date() ] },
1000*60*60
]
}
}
}
]).exec(function (err, result) {
// Handle err
console.log(result);
});
or using the more affluent API:
Product.aggregate()
.project({
"name": 1,
"dateExpired": "$date_expired",
"dateNow": { "$literal": new Date() },
"hoursToExpiry": {
"$divide": [
{ "$subtract": [ "$date_expired", new Date() ] },
1000*60*60
]
}
})
.exec(function (err, result) {
// Handle err
console.log(result);
});

Resources