Please Note: This tutorial assumes you have a working knowledge of javascript, the node run-time environment, mongoose queries and mongoDB
The Challenge:
So you work for a sales company called Super Doors as a developer and your boss, a Senior dev has been tasked with finding all customers that were unfortunate enough to purchase some bulletproof doors from the 24th - 30th of the month. Turns out the doors weren't so super after all and management needs a list of all the purchases made within those days for proper damage control. Lucky for you, your boss can't be bothered with such tasks and he assigns it to you. Next he goes, 'you know what? Why not just make it a feature on the platform for users to be able to query the database for customers within any range of dates?'
On the frontend:
You're meant to build a date range component that sends the startDate and the endDate to the backend. I won't go through building this as our concern is how to handle using dates on the backend to query the database. The point here is to define what values we're expecting as request parameters for the query.
Approach:
It's always a good idea to actually write out in words how you intend to approach the problem. You can be as specific or vague about it as you want, as long as it keeps you on track. Here's mine:
- check that date is not empty
- check that date is in proper format for query
- Query database using Mongoose
- Handle response.
On the backend.
We would need a Transaction model to hold information about each transaction made and a reference to an arbitrary User model. We would create a folder called 'models' and within it create a js file called 'transaction.js'.
Transaction Model:
const mongoose = require('mongoose');
const transactionSchema = mongoose.Schema({
_id:{ type: mongoose.Schema.Types.ObjectId, ref: 'User'},
name: String,
email: {
type: String,
unique: true,
match:`Regex match to ensure that string follows email format`
},
address: String,
phone: String,
door:{
name:String,
price: String
},
payment_status: {
type: String,
enum: ['Not Paid', 'Partial Payment', 'Paid'],
default: 'Not Paid'
},
date_paid: Date,
date_delivered: Date
});
module.exports = mongoose.model('Transaction', transactionSchema);
Controller Function:
Our controller function is where all the magic happens. Just like we did for the transaction.js file above, we would create a folder called 'controllers' and within it create a file called 'transaction.js'.
const Transaction = require('../models/transaction');
exports.getTransactionByDate = async(req, res) => {
try {
//get dates from req.query by es6 object destructuring
let { startDate, endDate } = req.query;
//1. check that date is not empty
if(startDate === '' || endDate === '') {
return res.status(400).json({
status:'failure',
message: 'Please ensure you pick two dates'
})
}
//2. check that date is in the right format
//expected result: YYY-MMM-DDD
console.log({ startDate, endDate});
//In some cases you'll get a date-time format where you have to separate the date
//from the time.
//3. Query database using Mongoose
//Mind the curly braces
const transactions = Transaction.find({
date_paid: {
$gte: new Date(new Date(startDate).setHours(00, 00, 00))
$lt: new Date(new Date(endDate).setHours(23, 59, 59))
}
}).sort({ date_paid: 'asc'})
}
//4. Handle responses
if(!transactions) {
return res.status(404).json({
status:'failure',
message:'Could not retrieve transactions'
})
}
res.status(200).json({
status:'success',
data: transactions
})
} catch(error) {
return res.status(500).json({
status:'failure',
error: error.message
})
}
}
Woah, okay, slow down...what just happened?
- We got the required parameters from our request query object:
let { startDate, endDate } = req.query;
- We checked that neither parameters were empty and handled the response should that be the case.
if(startDate === '' || endDate === '') {
return res.status(400).json({
status:'failure',
message: 'Please ensure you pick two dates'
})
}
//2. check that date is in the right format
//expected result: YYY-MMM-DDD
console.log({ startDate, endDate});
It is easier to get the date in Year-Month-Date format since that is the default way javascript handles dates
- We queried the database using the mongoose find and sort method. We also made use of mathematical operations, namely 'greater than or equal to'--$gte and 'less than'--$lt. The sort method was just a little extra I added to sort our list of transactions in ascending order, it's not inherently necessary.
const transactions = Transaction.find({
date_paid: {
$gte: new Date(new Date(startDate).setHours(00, 00, 00))
$lt: new Date(new Date(endDate).setHours(23, 59, 59))
}
}).sort({ date_paid: 'asc'})
}
Now you may be confused by what this new Date(new Date(startDate).setHours(00, 00, 00)) and this new Date(new Date(endDate).setHours(23, 59, 59)) does...Don't worry, got you covered.
When you call new Date() and pass it a date in string format, it returns the date you passed to it in date-time format. Then when we call new Date('date').setHours(); we get the chance to specify the time of day we want to start searching from in hour-minute-second format. In the case above, we want to start searching from the very beginning of our start date, so we have new Date('date').setHours(00,00,00);. Here's a short snippet from my PC to explain it further:
In the same manner, we want to get transactions up to but not beyond the last minute of our endDate, so we type new Date('date').setHours(23,59,59);.
But that's not all is it? We still have to wrap our new Date().setHours() in another new Date(). Why? Because as you can see from the snippet above, new Date().setHours() gives us what we want in seconds!(or is it milliseconds? I'm not sure). The point is, that's not the format we want, it's not legible enough.
So we wrap up new Date().setHours() to become new Date(new Date().setHours()) and we get our date in date-time format to the specific time we want!
- We handle responses. Both for success and failure. In three statements: -If we don't get our transactions, notify the client. -If we get our transactions however, return the data to the client side. -All other exceptions, catch!
if(!transactions) {
return res.status(404).json({
status:'failure',
message:'Could not retrieve transactions'
})
}
res.status(200).json({
status:'success',
data: transactions
})
} catch(error) {
return res.status(500).json({
status:'failure',
error: error.message
})
}
If you want to search for records within a single day, it's pretty much the same concept, only this time you have the same startDate and endDate, the difference would be in the time part of the query
Our Router:
Here we listen for get requests on the specified path of our choice and tell our getTransactionByDate function to handle the event for us.
const express = require('express');
const { getTransactionByDate } = require('../controllers/transaction');
const API = process.env.BASE_URL;
const router = express.Router();
router.get(`${API}/date_range`, getTransactionByDate);
module.exports = router;
I hope this has been helpful. If it has, like and comment! Also share to anyone you think may be having a hard time with querying NoSQL databases within a specific date range.
Top comments (7)
Actually that query is not exactly correct... in this form you will be missing 1 second worth of data... all the records between the 23:59:59 of the
endDate
and the 00:00:00 of the next dayIf you want you're search to be "endDate day inclusive" than you should add a day to the
endDate
and use$lt
with the 00:00:00 of that date, otherwise you should stick with a "endDate day exclusive" search and use the 00:00:00 of the endDate for the$lt
part of the query (just make sure that user knows that).Another point you should've made present (hope this is English enough) would be that this is the only correct way to do a search for a value in an interval, be it for
Date
or other datatype, and that a query like:would produce other (unexpected and undesired) results.
Later edit: actually now I have to make sure if this is true, but I'm very confident that it is.
Later-later edit: actually I was right. You can see some sample tests here (for the query in the right way) and here (for the query in the "wrong form"/that produces the undesired results).
Side note: actually, if you think about it, making the search "endDate day inclusive" is pretty counter-intuitive from a user-experience point of view, if you'd want to get a day's worth of data you'd have to set both the
startDate
andendDate
to the same value ... just my 2 cents.Call me picky, but I prefer sticking with the same syntax that I use in direct queries to a MongoDB:
Instead of
(Also, I wonder if that actually works, since there is a comma missing)
I'd use:
But again, it's me probably just being notoriuosly picky
I was recently facing the very same problem. You made it quite clear
I'm glad it helped!
Great article
Please can someone provide the client side implementation please
The code was exquisite but you forgot to await. Thanks