Why
Build?
As
we already know the whole world is suffering from COVID-19 and the vaccinations
are going in full swing everywhere. Finding a slot is getting tougher in our
country India as we have a huge population to be vaccinated. Numerous times we
have to go to CoWin site to search for a slot and slots are always full. It is
pretty time-consuming and irritating. Being a developer, I thought most of the
time is usually spent by us in the terminal so why can't we have a basic
terminal-based app to save time. So this post will help you in two ways
- Learn
how to create Node.js based CLI's
- Get real-time info about vaccination slots for your area.
Let's begin our initial setup!
Pre-requisite – We are
assuming you have installed Node.js and npm, If not you can install from here
So as a first step lets initialize our
project using command
npm init
Enter the basic details as shown below.
This will create package.json file in the
folder cowinCLI. The next step is to create a bin folder that will have our
index.js file containing our application.
Open the index.js file and add the
below-mentioned first line. This actually tells the interpreter that whatever
code runs below this will be handled by the node.
#! /usr/bin/env node
If you remember while creating we have
mentioned our entry point as index.js but actually, this file now exists in the
bin folder. So we will correct that as well as we will add one more entry. The
new entry which we will add is for the keyword we want to use to call our CLI.
We want to use something like cowin. So we will add this entry.
"bin": {
"cowin":
"./bin/index.js"
}
So your package.json will look something like
this
{
"name":
"vaccli",
"version":
"1.0.0",
"description":
"CLI vaccination slots",
"main":
"bin/index.js",
"scripts":
{
"test":
"echo \"Error: no test specified\" && exit 1"
},
"author":
"Nabheet",
"license":
"ISC",
"dependencies":
{
"axios":
"^0.21.1",
"chalk":
"^4.1.1",
"commander":
"^7.2.0",
"inquirer":
"^8.0.0",
"node-notifier":
"^9.0.1",
"tty-table":
"^4.1.3"
},
"bin":
{
"cowin":
"./bin/index.js"
}
}
So the basic structure is set. Now before we
start adding the functionality we have not given a thought to how we will fetch
the data? Let's first check that.
Do we have an API to fetch covid vaccination
slots data?
Thank God for looking at Co-Win site
they have provided us with OpenAPI. 50% of work is done. Now, all we need to do is,
consume this data and work as per our need. Let's now think about what our CLI
will do.
What functions our CLI will perform?
On looking closely at the calendar slots api for a district( In India we have a
Country comprised of States and Union Territories which in turn consists of
districts) we can see it needs some kind of district id.
So looking at how do we get districts id we
found another api but that needs state id
How do we get state id's another API😊
So our CLI shall do the following.
- Ability to get
all states and id's
- Ability to get
all district id's for a state id
- Ability to get
slots by district id
- Ability to
filter slots by ages as we have slots by 18-45 and 45 and above.
- Apart from this
some beautification
- Desktop
notification
In order to achieve this, we will be using
multiple npm modules lets install them first using the
below-mentioned command
npm install axios chalk commander inquirer
node-notifier tty-table
Packages to be installed
- Axios –for
calling the different API's
- Chalk – for
beautifying the console output
- Commander –
giving the different options and command in CLI such as cowin states or
cowin districts <state id>;
- Inquirer – for
getting user input for entering the age filter
- Node-notifier –
send desktop notification
- Tty-table –
format our table output
Let's begin by creating separate functions
for them. Create a util folder under the cowinCLI project. Create files
states.js, districts.js, config.js, and slots.js in util folder. Config.js is
for the configuration related common data such as table header formatting which
will be used by all functions
// Common configuration data to be used by
all functions.
exports.config = {
headers:
{ "User-Agent": "Axios - console app" },
};
exports.options = {
borderStyle:
"solid",
borderColor:
"blue",
headerAlign:
"center",
align:
"left",
color:
"white",
truncate:
"...",
width:
"90%",
};
Let's first code our reusable States function
in states.js
If you look, we need to call the states API for that we will use our already installed npm
package axios. We are calling the API and once we got a response we
are formatting the table data by using the tty-table package and writing the
output to the console. So this function will return the formatted output of
states and its id's.
const axios = require('axios');
const table = require("tty-table");
const { config,options } = require('./config');
// function to return list of all states
module.exports = function() {
axios
.get("https://cdn-api.co-vin.in/api/v2/admin/location/states",
config)
.then((response) => {
//
table formatter
let
header = [
{
value: "state_id",
headerColor: "cyan",
alias: "State ID",
color: "white",
align: "left",
width: 40,
},
{
value: "state_name",
alias: "State",
headerColor: "cyan",
color: "white",
align: "left",
width: 40,
},
];
const
out = table(header, response.data.states, options).render();
console.table(out);
})
.catch((error) => {
console.log(error);
});
};
Let's code our second reusable Districts
function in districts.js
For this also we will use a similar set up of
axios and tty-table. The only thing to be noted is, in this function which we
are exporting has an argument as stateid.
const axios = require('axios');
const table = require("tty-table");
const { config,options } = require('./config');
// Function which take stateid as input and
return all the formatted districts
module.exports = function(stateid) {
axios
.get(
`https://cdn-api.co-vin.in/api/v2/admin/location/districts/${stateid}`,
config
)
.then((response) => {
//
Table header specific formatting
let
header = [
{
value: "district_id",
headerColor: "cyan",
alias: "District ID",
color: "white",
align: "left",
width: 40,
},
{
value: "district_name",
alias: "District",
headerColor: "cyan",
color: "white",
align: "left",
width: 40,
},
];
//
Output the results.
const
out = table(header, response.data.districts, options).render();
console.table(out);
})
.catch((error) => {
console.log(error);
});
};
Let's code our third reusable slots function
in slots.js
For this also we will use a similar set up of
axios and tty-table. The only thing to be noted is in this function which we
are exporting has an argument as districtid. In addition to it you can see we
are using chalk and inquirer package. Chalk is used to format the headers above
the table and inquirer is used for taking input from user when slots command is
run. We have also used node-notifier which will send desktop notification as
soon as it runs, just an example. You can modify this behavior to code your own
custom logic.
const axios = require('axios');
const table = require("tty-table");
const chalk = require("chalk");
const notifier = require("node-notifier");
var inquirer = require("inquirer");
const { config,options } = require('./config');
// function to check slots.
module.exports = function(district) {
//Input
prompt for getting what age you want to check records.
inquirer
.prompt([
{
type: "list",
name: "choice",
message: "Which Age group?",
choices: [
{
name: "View All",
value: "",
},
{
name: "45 Plus",
value: "45",
},
{
name: "18 - 45 ",
value: "18",
},
],
},
])
.then((answers) => {
const
date = new Date();
var
todaysDate = `${date.getDate()}-${String(
date.getMonth() + 1
).padStart(2, "0")}-${date.getFullYear()}`;
console.log(
chalk.underline.bgRed.bold(`Showing Slots from - ${todaysDate}`)
);
axios
.get(
`https://cdn-api.co-vin.in/api/v2/appointment/sessions/public/calendarByDistrict?district_id=${district}&date=${todaysDate}`,
config
)
.then((response) => {
let finalData = [];
let districtName;
response.data.centers.forEach((item) => {
item.sessions.forEach((session) =>
{
districtName = item.district_name;
// based on user age choice filter the data
if (answers.choice == "") {
let data = {
Center: item.name,
Address: item.address,
Date: session.date,
FreeSlots:
session.available_capacity,
Age: session.min_age_limit,
};
finalData.push(data);
} else if (
answers.choice == "18"
&&
session.min_age_limit == "18"
) {
let data = {
Center: item.name,
Address: item.address,
Date: session.date,
FreeSlots:
session.available_capacity,
Age: session.min_age_limit,
};
finalData.push(data);
} else if (
answers.choice == "45"
&&
session.min_age_limit == "45"
) {
let data = {
Center: item.name,
Address: item.address,
Date: session.date,
FreeSlots:
session.available_capacity,
Age: session.min_age_limit,
};
finalData.push(data);
}
});
});
console.log(
chalk.underline.bgGreen.bold(`District - ${districtName}`)
);
switch (answers.choice) {
case "":
console.log(chalk.underline.bgBlue.bold(`All ages`));
break;
case "45":
console.log(chalk.underline.bgBlue.bold(`45+ Age`));
break;
case "18":
console.log(chalk.underline.bgBlue.bold(`18-45 Age`));
break;
default:
break;
}
// table formatting
let header = [
{
value: "Center",
headerColor: "cyan",
color: "white",
align: "left",
width: 40,
},
{
value: "Address",
headerColor: "cyan",
color: "white",
align: "left",
width: 40,
},
{
value: "Date",
headerColor: "cyan",
color: "white",
align: "left",
width: 15,
},
{
value: "FreeSlots",
headerColor: "cyan",
color: "white",
align: "left",
width: 20,
},
{
value: "Age",
headerColor: "cyan",
color: "white",
align: "left",
width: 20,
},
];
const out = table(header, finalData, options).render();
console.table(out);
notifier.notify({
title: "Vaccination Slots Available",
subtitle: "Daily Maintenance",
message: "Immediately go and check Vaccination slots!",
wait: true,
});
})
.catch((error) => {
console.log(error);
});
})
.catch((error) => {
if
(error.isTtyError) {
// Prompt couldn't be rendered in the current environment
}
else {
// Something else went wrong
}
});
};
Now our all basic functions are in place but
what is pending is the actual CLI😊 Let's start building that.
Lets now build the CLI by updating index.js
So far we have used all npm packages except
commander it is the heart of our CLI. We will be using commander for making the
subcommands as well as flag options. As can be seen, below we have used both command
and option. Commands for getting states, districts, and slots and they have a
callback function mentioned as our reusable functions under action.
#! /usr/bin/env node
const program = require("commander");
// import all functions
const districts = require('../util/districts');
const states = require('../util/states');
const slots = require('../util/slots');
// adding different cli options,commands and
passing callback functions in actions
program.option("-a, --available", "Output
If slots available");
program
.command("states")
.description("Get all State Codes and descriptions.")
.action(states);
program
.command("district <stateid>")
.description("Get all district of a State")
.action(districts);
program
.command("slots <districtid>")
.description("Get slots for the district")
.action(slots);
program.parse();
Final touches
So we have everything ready all we need to do
is now run the below command which will install our package globally.
npm install -g .
Output
cowin states
cowin districts 12
cowin slots 187
Comments
Post a Comment