diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..68d1bbf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_style = tabs +indent_size = 4 diff --git a/.gitignore b/.gitignore index a7b784b..504ca24 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ debug.log .DS_Store .now .env +dist/ \ No newline at end of file diff --git a/now.json b/now.json index a1d3599..87b780c 100644 --- a/now.json +++ b/now.json @@ -1,19 +1,23 @@ { - "version": 2, - "builds": [{ - "src": "app.js", - "use": "@now/node-server" - }], - "routes": [{ - "headers": { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "X-Requested-With, Content-Type, Accept" - }, - "src": "/.*", - "dest": "/app.js" - }], - "env": { - "VERSION": "1" - } + "version": 2, + "builds": [ + { + "src": "src/api.ts", + "use": "@vercel/node" + } + ], + "routes": [ + { + "headers": { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "X-Requested-With, Content-Type, Accept" + }, + "src": "/(.*)", + "dest": "src/api.ts" + } + ], + "env": { + "VERSION": "1" + } } diff --git a/src/api.ts b/src/api.ts index 066315d..1057cb4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,7 @@ import express from "express"; import morgan from "morgan"; import { errorHandler } from "./api/errorHandler"; +import { plainRouter } from "./api/plainRouter"; import { router } from "./api/router"; import { userAgentMiddleware } from "./api/userAgent"; @@ -11,11 +12,19 @@ const app = express(); app.use(morgan("common")); app.use(userAgentMiddleware); -app.use("/", router); +/** + * Plain CMD/Basic routes have both quiet and full modes + * Same with regular / routes with ansi color codes + */ +app.use(["/quiet/basic", "/quiet/cmd", "/quiet/plain"], plainRouter); +app.use(["/basic", "/cmd", "/plain"], plainRouter); + +app.use(["/quiet", "/"], router); app.use("/", errorHandler); +// Not found handler app.use("*", (_req, res) => - res.send( + res.status(404).send( `Welcome to COVID-19 Tracker CLI v${version} by Waren Gonzaga with Wareneutron Developers\n Please visit: https://warengonza.ga/covid19-tracker-cli\n` ) diff --git a/src/api/errorHandler.ts b/src/api/errorHandler.ts index 1d7c884..71533dc 100644 --- a/src/api/errorHandler.ts +++ b/src/api/errorHandler.ts @@ -1,9 +1,13 @@ import { ErrorRequestHandler } from "express"; -import { generateColorTable } from "../utils/generateTable"; +/** + * + * @param error Error object, received from errors thrown in the code + * @param res Response object from Express + */ export const errorHandler: ErrorRequestHandler = (error, _, res, _next) => { const statusCode = res.statusCode === 200 ? 500 : res.statusCode; res.status(statusCode); - res.send(generateColorTable([error.message], "red")); + res.send(error.message + "\n"); res.end(); }; diff --git a/src/api/handleAsync.ts b/src/api/handleAsync.ts index aa6993a..cd05372 100644 --- a/src/api/handleAsync.ts +++ b/src/api/handleAsync.ts @@ -1,5 +1,11 @@ import { Request, Response, NextFunction } from "express"; type Handler = (req: Request, res: Response, next: NextFunction) => R; + +/** + * @example router.use("/path/", handleAsync((req, res, next)=>{res.send("Hello World!")})); + * @param asyncFn An asyncronous function that takes in req, res, and next + * @returns An asyncronous function where errors will be catched and sent to the error handler + */ const handleAsync: (asyncFn: Handler>) => Handler = ( asyncFn ) => { diff --git a/src/api/plainRouter.ts b/src/api/plainRouter.ts new file mode 100644 index 0000000..c65d840 --- /dev/null +++ b/src/api/plainRouter.ts @@ -0,0 +1,61 @@ +import { Router } from "express"; +import handleAsync from "./handleAsync"; +import { + globalInformationPlain, + informationPerCountryPlain, + historyPerCountryPlain, + globalHistoryPlain, +} from "../utils/plainHandlers"; +import { isQuiet } from "./router"; + +/** + * The plainRouter handles all the plain routes such as /basic, /cmd, and /plain + * It also handles the quiet version of these routes + */ +export const plainRouter = Router({ mergeParams: true }); + +plainRouter.get( + "/history/:mode?", + handleAsync(async (req, res, next) => { + // get mode from params + let mode = req.params.mode as "cases" | "deaths" | "recovered"; + + //default to cases if mode is undefined + mode = mode === undefined ? "cases" : mode; + + // if the mode is not in the api then return to next handler + if (!["cases", "deaths", "recovered"].includes(mode)) return next(); + res.send(await globalHistoryPlain(mode, isQuiet(req))); + }) +); + +plainRouter.get( + "/history/:country/:mode?", + handleAsync(async (req, res, next) => { + const country = req.params.country; + // get mode from params + let mode = req.params.mode as "cases" | "deaths" | "recovered"; + + //default to cases if mode is undefined + mode = mode === undefined ? "cases" : mode; + + // if the mode is not in the api then return to next handler + if (!["cases", "deaths", "recovered"].includes(mode)) return next(); + res.send(await historyPerCountryPlain(country, mode, isQuiet(req))); + }) +); + +plainRouter.get( + "/:country", + handleAsync(async (req, res, _next) => { + const country = req.params.country; + res.send(await informationPerCountryPlain(country, isQuiet(req))); + }) +); + +plainRouter.get( + "/", + handleAsync(async (req, res, _next) => { + res.send(await globalInformationPlain(isQuiet(req))); + }) +); diff --git a/src/api/router.ts b/src/api/router.ts index 5eead3b..c767f3e 100644 --- a/src/api/router.ts +++ b/src/api/router.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Request, Router } from "express"; import { globalHistory, globalInformation, @@ -7,6 +7,14 @@ import { } from "../utils/handlers"; import handleAsync from "./handleAsync"; +/** + * + * @param req Express request + * @returns Boolean if the request starts with /quiet + */ +export const isQuiet: (req: Request) => boolean = (req) => + req.baseUrl.startsWith("/quiet"); + /** * The rootRouter handles all the processing of the requests *after* passing through * all middlewares except not found and error handling middleware @@ -19,13 +27,13 @@ router.get( handleAsync(async (req, res, next) => { // get mode from params let mode = req.params.mode as "cases" | "deaths" | "recovered"; + //default to cases if mode is undefined mode = mode === undefined ? "cases" : mode; - console.log(mode); // if the mode is not in the api then return to next handler if (!["cases", "deaths", "recovered"].includes(mode)) return next(); - res.send(await globalHistory(mode)); + res.send(await globalHistory(mode, isQuiet(req))); }) ); @@ -33,7 +41,6 @@ router.get( "/history/:country/:mode?", handleAsync(async (req, res, next) => { const country = req.params.country; - console.log("eere"); // get mode from params let mode = req.params.mode as "cases" | "deaths" | "recovered"; @@ -42,7 +49,7 @@ router.get( // if the mode is not in the api then return to next handler if (!["cases", "deaths", "recovered"].includes(mode)) return next(); - res.send(await historyPerCountry(country, mode)); + res.send(await historyPerCountry(country, mode, isQuiet(req))); }) ); @@ -50,13 +57,13 @@ router.get( "/:country", handleAsync(async (req, res, _next) => { const country = req.params.country; - res.send(await informationPerCountry(country)); + res.send(await informationPerCountry(country, isQuiet(req))); }) ); router.get( "/", - handleAsync(async (_req, res, _next) => { - res.send(await globalInformation()); + handleAsync(async (req, res, _next) => { + res.send(await globalInformation(isQuiet(req))); }) ); diff --git a/src/api/userAgent.ts b/src/api/userAgent.ts index 85d996f..b3c65ad 100644 --- a/src/api/userAgent.ts +++ b/src/api/userAgent.ts @@ -1,4 +1,5 @@ import { Request, Response, NextFunction } from "express"; +const { version } = require("../../package.json"); // Type of middleware and handler export type Handler = ( @@ -7,6 +8,11 @@ export type Handler = ( next: NextFunction ) => Promise | void; +/** + * + * @param userAgent The user agent of the requester + * @returns A boolean that is true of the user agent provided is from curl / wget / httpie + */ const isTerminal: (userAgent: string | undefined) => boolean = (userAgent) => { if (userAgent === undefined) return false; if (/curl|wget|httpie/i.test(userAgent)) return true; @@ -14,10 +20,16 @@ const isTerminal: (userAgent: string | undefined) => boolean = (userAgent) => { }; export const userAgentMiddleware: Handler = (req, res, next) => { + /** + * Get the user agent from the request + * Determine if the user agent is from curl / wget / httpie + * If true then proceed using the next function + * Else return with message + */ const userAgent = req.headers["user-agent"]; if (!isTerminal(userAgent)) { res.send( - `Welcome to COVID-19 Tracker CLI v3.9.3 by Waren Gonzaga.\n\nPlease visit: https://warengonza.ga/covid19-tracker-cli` + `Welcome to COVID-19 Tracker CLI v${version} by Waren Gonzaga with Wareneutron Developers\nPlease visit: https://warengonza.ga/covid19-tracker-cli\n` ); return; } diff --git a/src/cli.ts b/src/cli.ts index d6f3923..f7714b9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,14 +1,19 @@ import argv from "minimist"; -import { generateColorTable } from "./utils/generateTable"; import { globalHistory, globalInformation, historyPerCountry, informationPerCountry, } from "./utils/handlers"; +import { + globalHistoryPlain, + globalInformationPlain, + historyPerCountryPlain, + informationPerCountryPlain, +} from "./utils/plainHandlers"; const args = argv(process.argv.slice(2)); -let { history, mode, help } = args; +let { history, mode, help, quiet, plain } = args; const country = args._[0]; const { version } = require("../package.json"); @@ -22,29 +27,49 @@ Country: Can be a country name or ISO 3166-1 alpha-2 country code Options: --history Show a chart of country's cases of world's cases - --mode Use with --history to make show a chart of cases, deaths, or recovered`; + --mode Use with --history to make show a chart of cases, deaths, or recovered + --quiet Only show necessary information + --plain Enable plain mode`; let output: string = ""; const main = async () => { if (help) return console.log(helpMessage); + quiet = quiet === undefined ? false : quiet; - if (history === undefined || typeof history === "undefined") { - if (country === undefined) output = await globalInformation(); - else output = await informationPerCountry(country); + if (history === undefined) { + if (country === undefined) { + output = + plain === true + ? await globalInformationPlain(quiet) + : await globalInformation(quiet); + } else { + output = + plain === true + ? await informationPerCountryPlain(country, quiet) + : await informationPerCountry(country, quiet); + } } - mode = mode === undefined || typeof mode === "undefined" ? "cases" : mode; // defauilt to cases if mode is not present + mode = mode === undefined ? "cases" : mode; // default to cases if mode is not present if (!["cases", "deaths", "recovered"].includes(mode)) mode === "cases"; // default to cases if mode is not cases | deaths | recovered if (history) { - if (country === undefined) output = await globalHistory(mode); - else output = await historyPerCountry(country, mode); + if (country === undefined) { + output = + plain === true + ? await globalHistoryPlain(mode, quiet) + : await globalHistory(mode, quiet); + } else { + output = + plain === true + ? await historyPerCountryPlain(country, mode, quiet) + : await historyPerCountry(country, mode, quiet); + } } console.log(output); }; main().catch((err) => { - let errorTable = generateColorTable([err.message], "red"); - console.log(errorTable); + console.log(err.message + "\n"); }); diff --git a/src/utils/generateAsciichart.ts b/src/utils/generateAsciichart.ts index af24ef3..6f4ef59 100644 --- a/src/utils/generateAsciichart.ts +++ b/src/utils/generateAsciichart.ts @@ -1,6 +1,10 @@ import { plot } from "asciichart"; -export const generateAsciichart: (data: number[]) => string = (data) => { +export const generateAsciichart: ( + data: number[], + removePadding?: boolean, + height?: number +) => string = (data, removePadding, height = 10) => { // Divide the data by 100 since asciichart runs out of ram let casesArray: number[] = []; data.forEach((int) => { @@ -9,7 +13,7 @@ export const generateAsciichart: (data: number[]) => string = (data) => { }); // Generate chart - let chart = plot(casesArray, { height: 10 }); + let chart = plot(casesArray, { height }); // Get and normalize the floats let floatsInAsciiChart: string[] = chart.match(/[+-]?\d+(\.\d+)?/g)!; // Get floats from asciichart @@ -25,5 +29,13 @@ export const generateAsciichart: (data: number[]) => string = (data) => { chart = chart.replace(key, value); }); + // Remove the padding if the user requests to + if (removePadding === true) { + chart = chart + .split("\n") + .map((str) => str.trimStart()) + .join("\n"); + } + return chart; }; diff --git a/src/utils/addFooterAndGenerateChart.ts b/src/utils/generateOutput.ts similarity index 53% rename from src/utils/addFooterAndGenerateChart.ts rename to src/utils/generateOutput.ts index bc6a411..dd1360f 100644 --- a/src/utils/addFooterAndGenerateChart.ts +++ b/src/utils/generateOutput.ts @@ -9,26 +9,35 @@ const { version } = require("../../package.json"); * @param chartType The type of chart that will be placed on the header * @param updateTime The unix timestamp from the API of when the data was last updated * @param data The data formatted into tables + * @param quiet Optional, set to true if the user does not want unnecessary information * @returns A string containing a formatted table */ -export const addFooterAndGenerateChart: ( +export const generateOutput: ( chartType: string, updateTime: number, - data: (string | string[])[] -) => string = (chartType, updateTime, data) => { + data: (string | string[])[], + quiet?: boolean +) => string = (chartType, updateTime, data, quiet) => { + quiet = quiet === undefined ? true : quiet; let header = `COVID-19 Tracker CLI v${version} - ${chartType}`; let timestamp = getTimestamp(updateTime).yellow; - data.unshift(header, timestamp); - data = data.concat([ - "Help: Try to append the URL with /help to learn more...", - "Source: https://disease.sh/v3/covid-19/", - "Code: https://github.com/wareneutron/covid19-tracker-cli", - ]); + data.unshift(timestamp); + if (!quiet) data.unshift(header); + + if (!quiet) + data = data.concat([ + "Help: Try to append the URL with /help to learn more...", + "Source: https://disease.sh/v3/covid-19/", + "Code: https://github.com/wareneutron/covid19-tracker-cli", + ]); let response = generateColorTable(data, "cyan"); - response += `\n${getSaying().green}\n`; //saying - response += `\n${"═".repeat(60)}\n`; + if (!quiet) { + response += `\n${getSaying().green}\n`; //saying + response += `\n${"═".repeat(60)}\n`; + } + response += `Love this project? Help us to help others by means of coffee!\n`; // support msg // Include GCash message if the query is to the PH @@ -38,12 +47,15 @@ export const addFooterAndGenerateChart: ( // @ts-expect-error: Missing type definitions causes TS to highlight brightRed response += `(Buy Me A Coffee) warengonza.ga/coffee4dev\n`.brightRed; //BMC link - response += `${"═".repeat(60)}\n`; - response += `Follow me on twitter for more updates!\n`; - response += - ["@warengonzaga", "#covid19trackercli"] - .map((text) => text.black.bgCyan) - .join(" ") + "\n"; + + if (!quiet) { + response += `${"═".repeat(60)}\n`; + response += `Follow me on twitter for more updates!\n`; + response += + ["@warengonzaga", "#covid19trackercli"] + .map((text) => text.black.bgCyan) + .join(" ") + "\n"; + } return response; }; diff --git a/src/utils/generatePlainOutput.ts b/src/utils/generatePlainOutput.ts new file mode 100644 index 0000000..945ec2b --- /dev/null +++ b/src/utils/generatePlainOutput.ts @@ -0,0 +1,99 @@ +import { PlainData } from "./getInformation"; +import { getSaying } from "./getSaying"; +import { getTimestamp } from "./getTimestamp"; +const { version } = require("../../package.json"); + +/** + * @param info The plain data that will be shown at the top in two columns + * @param chartType The type of chart that will be shown. Ex: "Global Update", "Philippine Historical Chart" + * @param quiet Boolean, set to true if the user requsted quiet mode + * @param extraRows Any extra rows that will be presented under the main info. Used for Asciichart + * @returns A string showing the provided data and configuration + */ +export const generatePlainOutput: ( + info: PlainData, + chartType: string, + quiet: boolean, + extraRows?: string[] +) => string = ({ data, metainfo }, chartType, quiet, extraRows) => { + // Set line depending if it contains a chart or not + let line = extraRows === undefined ? "-".repeat(60) : "-".repeat(68); + line += "\n"; + + let header = `COVID-19 Tracker CLI v${version} - ${chartType}`; + let timestamp = getTimestamp(metainfo.updated as number); + let saying = getSaying(); + + // Include GCash message if the query is to the PH + let GCashMessage = chartType.toLowerCase().includes("philippines") + ? "(GCash) +639176462753\n" + : ""; + + // Generate table + let table = ""; + + // Create columns + let normalizedArray: string[] = []; + Object.keys(data).forEach((key) => { + let value = data[key]; + let line = `${key.padEnd(15, " ")}| ${value.padEnd(13, " ")}`; // create a line with length 30; + normalizedArray.push(line); + }); + + while (normalizedArray.length > 0) { + let left = normalizedArray.shift(); + let right = normalizedArray.shift(); + + //right may be undefined, so default to empty string + if (right === undefined) right = ""; + + table += `${left}${right}`; + if (normalizedArray.length !== 0) table += `\n`; // do not add whitespace at the end of the table + } + + // responseArray is the array of the raw data **before** adding the separator lines + let responseArray: string[] = [timestamp, table]; + if (!quiet) responseArray.unshift(header); + + // Add extraRows to responseArray + if (extraRows !== undefined) { + extraRows.forEach((str) => { + responseArray.push(str); + }); + } + + // Add the help msg and other messages + if (!quiet) + responseArray = responseArray.concat([ + "Help: Try to append the URL with /help to learn more...", + "Source: https://www.worldometers.info/coronavirus/", + "Code: https://github.com/warengonzaga/covid19-tracker-cli", + `\n${saying}\n`, + ]); + + responseArray.push( + `Love this project? Help us to help others by means of coffee!\n${GCashMessage}(Buy Me A Coffee) warengonza.ga/coffee4dev` + ); + + if (!quiet) + responseArray.push( + `Follow me on twitter for more updates!\n@warengonzaga #covid19trackercli` + ); + + // Construct the final output + let response: string = "\n"; + responseArray.forEach((str) => { + response += `${line}`; + response += `${str}\n`; + }); + + // Add padding to the side + response = response + .split("\n") + .map((str) => ` ${str}`) + .join("\n"); + + response += "\n"; + + return response; +}; diff --git a/src/utils/generateTable.ts b/src/utils/generateTable.ts index 0fa579a..ae65f8f 100644 --- a/src/utils/generateTable.ts +++ b/src/utils/generateTable.ts @@ -176,7 +176,7 @@ const separatorHandler: ( ); } - return "If you have reached this then something has gone wrong"; + throw new Error("Separator handler conditions failed"); }; /** diff --git a/src/utils/getInformation.ts b/src/utils/getInformation.ts new file mode 100644 index 0000000..9757690 --- /dev/null +++ b/src/utils/getInformation.ts @@ -0,0 +1,171 @@ +import axios from "axios"; +axios.defaults.baseURL = "https://disease.sh/v3/covid-19"; + +let countryCodes: { [key: string]: string } = {}; +(async () => { + countryCodes = (await axios.get("http://country.io/names.json")).data; +})(); + +export interface PlainData { + data: { + [key: string]: string; + }; + metainfo: { + [key: string]: number | string; + }; +} + +/** + * @param isPlain Set to true to recieve an object containing the responses instead of the rows + * @returns an object containing the data and metainfo **if isPlain is set to true** + * @returns an array in the format of [timestamp, rows] **if isPlain is set to false** + */ +export const getAllInfo: ( + isPlain?: boolean +) => Promise<[number, (string[] | string)[]] | PlainData> = async ( + isPlain = false +) => { + let { data: globalData } = await axios.get("/all"); + let { cases, deaths, recovered, updated } = globalData; + + let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; + let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; + + [cases, deaths, recovered] = [cases, deaths, recovered].map((num: number) => + num.toLocaleString("en-US", { maximumFractionDigits: 0 }) + ); + + // Return object containing information if isPlain is set to true + if (isPlain) { + return { + data: { + Cases: cases, + Deaths: deaths, + "Mortality %": mortalityPercentage, + Recovered: recovered, + "Recovered %": recoveredPercentage, + }, + metainfo: { + updated, + }, + }; + } + + // Return rows if isPlain is set to false + // prettier-ignore + return [updated, [ + ["Cases".magenta, "Deaths".red,"Recovered".green, "Mortality %".red,"Recovered %".green], + [cases, deaths, recovered, mortalityPercentage, recoveredPercentage]]] +}; + +/** + * @param country the country code or string that the user provides from req.params or CLI + * @param isPlain Set to true to recieve an object containing the responses instead of the rows + * @returns an object containing the data and metainfo **if isPlain is set to true** + * @returns an array in the format of [timestamp, API countryname, formal countryname, rows[]] **if isPlain is false** + */ +export const getCountryInfo: ( + country: string, + isPlain?: boolean +) => Promise< + [number, string, string, (string[] | string)[]] | PlainData +> = async (country, isPlain) => { + // Wait 1 second for countryCodes to initialize, needed for CLI + if (Object.keys(countryCodes).length === 0) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + } + + country = + country.length < 3 ? countryCodes[country.toUpperCase()] : country; // Convert country code to country name + + if (country === undefined || typeof country === "undefined") + throw new Error(`Cannot find provided country`); + + try { + let { data: countryData } = await axios.get(`/countries/${country}`); + // prettier-ignore + let { country: countryName, updated, cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical} = countryData; + + let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; + let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; + + // prettier-ignore + [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical ] = + [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical, + ].map((num: number) => + num.toLocaleString("en-US", { maximumFractionDigits: 0 }) + ); + + // Return object containing information if isPlain is set to true + if (isPlain) { + return { + data: { + Cases: cases, + "Today Cases": todayCases, + Active: active, + Recovered: recovered, + Deaths: deaths, + "Today Deaths": todayDeaths, + Critical: critical, + "Mortality %": mortalityPercentage, + "Recovery %": recoveredPercentage, + "Cases/Million": casesPerOneMillion, + }, + metainfo: { + updated, + countryName, + }, + }; + } + + //prettier-ignore + return [updated, country, countryName, [ + [ "Cases".magenta, "Deaths".red, "Recovered".green, "Active".blue, "Cases/Million".blue,], + [ cases, deaths, recovered, active, casesPerOneMillion,], + [ "Today Cases".magenta, "Today Deaths".red, "Critical".red, "Mortaility %".red, "Recovery %".green], + [ todayCases, todayDeaths, critical, mortalityPercentage, recoveredPercentage]] + ] + } catch { + throw new Error(`Cannot find the provided country`); + } +}; + +/** + * Get historical info about a country / the world + * @param mode - mode that the user requested + * @param country - countryname that the user requested, leave blank to get world data + * @returns an object containing date and chartData properties + */ +export const getHistorical: ( + mode: "cases" | "deaths" | "recovered", + country?: string +) => Promise<{ + date: string; + chart: number[]; +}> = async (mode, country = "all") => { + const { data: historicalData } = await axios.get(`/historical/${country}`); + + const data: { + [key: string]: number; + } = + country === "all" + ? historicalData[mode] + : historicalData["timeline"][mode]; + + // Get first and last date + const dates = Object.keys(data); + + // Label for chart + const date = `${ + mode.charAt(0).toUpperCase() + mode.slice(1) + } from ${dates.shift()} to ${dates.pop()}`; + + const chartData = Object.values(data); + + return { + date, + chart: chartData, + }; +}; diff --git a/src/utils/getSaying.ts b/src/utils/getSaying.ts index c62a359..4f34489 100644 --- a/src/utils/getSaying.ts +++ b/src/utils/getSaying.ts @@ -33,6 +33,9 @@ const sayings = [ "Wine is the most healthful and most hygienic of beverages - Louis Pasteur", ]; +/** + * @returns A random saying from the array of sayings + */ export const getSaying: () => string = () => { const index = Math.floor(Math.random() * sayings.length); return sayings[index]; diff --git a/src/utils/getTimestamp.ts b/src/utils/getTimestamp.ts index 879a777..a4b8aab 100644 --- a/src/utils/getTimestamp.ts +++ b/src/utils/getTimestamp.ts @@ -1,5 +1,4 @@ /** - * * @param timestamp Timestamp in Epoch Time * @returns String in form of As of MM/DD/YYYY, HH:mm:SS AM/PM [Date: MM/DD/YYYY] */ diff --git a/src/utils/handlers.ts b/src/utils/handlers.ts index 9560104..e6067b1 100644 --- a/src/utils/handlers.ts +++ b/src/utils/handlers.ts @@ -1,83 +1,6 @@ -import axios from "axios"; -import { addFooterAndGenerateChart } from "./addFooterAndGenerateChart"; +import { generateOutput } from "./generateOutput"; import { generateAsciichart } from "./generateAsciichart"; - -axios.defaults.baseURL = "https://disease.sh/v3/covid-19"; -let countryCodes: { [key: string]: string } = {}; -(async () => { - countryCodes = (await axios.get("http://country.io/names.json")).data; -})(); - -/** - * - * @returns an array in the format of [timestamp, rows] - */ -const getAllInfo: () => Promise<[number, (string[] | string)[]]> = async () => { - let { data: globalData } = await axios.get("/all"); - let { cases, deaths, recovered, updated } = globalData; - - let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; - let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; - - [cases, deaths, recovered] = [cases, deaths, recovered].map((num: number) => - num.toLocaleString("en-US", { maximumFractionDigits: 0 }) - ); - - // prettier-ignore - return [updated, [ - ["Cases".magenta, "Deaths".red,"Recovered".green, "Mortality %".red,"Recovered %".green], - [cases, deaths, recovered, mortalityPercentage, recoveredPercentage]]] -}; - -/** - * - * @param country the country code or string that the user provides from req.params or CLI - * @returns an array in the format of [timestamp, API countryname, formal countryname, rows[]] - */ -const getCountryInfo: ( - country: string -) => Promise<[number, string, string, (string[] | string)[]]> = async ( - country -) => { - // Wait 1 second for countryCodes to initialize, needed for CLI - if (Object.keys(countryCodes).length === 0) { - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - } - - country = - country.length < 3 ? countryCodes[country.toUpperCase()] : country; // Convert country code to country name - - if (country === undefined || typeof country === "undefined") - throw new Error(`Cannot find provided country`); - - try { - let { data: countryData } = await axios.get(`/countries/${country}`); - // prettier-ignore - let { country: countryName, updated, cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical} = countryData; - - let mortalityPercentage = ((deaths / cases) * 100).toFixed(2) + "%"; - let recoveredPercentage = ((recovered / cases) * 100).toFixed(2) + "%"; - - // prettier-ignore - [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical ] = - [ cases, deaths, recovered, active, casesPerOneMillion, todayCases, todayDeaths, critical, - ].map((num: number) => - num.toLocaleString("en-US", { maximumFractionDigits: 0 }) - ); - - //prettier-ignore - return [updated, country, countryName, [ - [ "Cases".magenta, "Deaths".red, "Recovered".green, "Active".blue, "Cases/Million".blue,], - [ cases, deaths, recovered, active, casesPerOneMillion,], - [ "Today Cases".magenta, "Today Deaths".red, "Critical".red, "Mortaility %".red, "Recovery %".green], - [ todayCases, todayDeaths, critical, mortalityPercentage, recoveredPercentage]] - ] - } catch { - throw new Error(`Cannot find the provided country`); - } -}; +import { getAllInfo, getCountryInfo, getHistorical } from "./getInformation"; /** * historyPerCountry shows a tablechart of the of a country @@ -85,39 +8,32 @@ const getCountryInfo: ( * Today Cases, Today Deaths, Critical, Mortality %, Recovery in a chart * @param country country code or country name that the user wants to query * @param mode Mode that the user wants to query must be: "cases" | "deaths" | "recoveries" + * @param quiet tells the response to be in quiet mode or not */ export const historyPerCountry: ( country: string, - mode: "cases" | "deaths" | "recovered" -) => Promise = async (country, mode) => { + mode: "cases" | "deaths" | "recovered", + quiet: boolean +) => Promise = async (country, mode, quiet) => { // Get summary info about a country - let [updated, apiCountryname, countryName, rows] = await getCountryInfo( + let [updated, apiCountryname, countryName, rows] = (await getCountryInfo( country - ); - let { data: historicalData } = await axios.get( - `/historical/${apiCountryname}` - ); - - // get data from API request based on the mode - let data = historicalData["timeline"][mode]; - - // Get first and last date of timeline - const firstDate = Object.keys(data).shift(); - const lastDate = Object.keys(data).pop(); + )) as [number, string, string, (string[] | string)[]]; - //generate historical graph - const chart = generateAsciichart(Object.values(data)).split("\n"); + // Fetch chart data and generate historical graph; + let historicalData = await getHistorical(mode, apiCountryname); + const chart = generateAsciichart(historicalData.chart).split("\n"); // add chart label and chart - // prettier-ignore - rows.push(`${ mode.charAt(0).toUpperCase() + mode.slice(1) } from ${firstDate} to ${lastDate}`.magenta); + rows.push(historicalData.date.magenta); rows = rows.concat(chart); - // generate table - let response = addFooterAndGenerateChart( + // Generate table + let response = generateOutput( `${countryName} Historical Chart`, updated, - rows + rows, + quiet ); return response; @@ -127,33 +43,31 @@ export const historyPerCountry: ( * globalHistory shows a tablechart of the cases of all the countries * Shows Cases, Deaths, Recovered, Active, Cases/Million * and a graph of a country's cases + * @param mode Mode that the user wants to query must be: "cases" | "deaths" | "recoveries" + * @param quiet tells the response to be in quiet mode or not */ export const globalHistory: ( - mode: "cases" | "deaths" | "recovered" -) => Promise = async (mode) => { + mode: "cases" | "deaths" | "recovered", + quiet: boolean +) => Promise = async (mode, quiet) => { // Get summary info - let [updated, rows] = await getAllInfo(); - - // Get data from API - const { data: historicalData } = await axios.get("/historical/all"); - const data: { - [key: string]: number; - } = historicalData[mode]; + let [updated, rows] = (await getAllInfo()) as [ + number, + (string[] | string)[] + ]; - const firstDate = Object.keys(data).shift(); - const lastDate = Object.keys(data).pop(); + // Fetch chart data and generate historical graph; + const historicalData = await getHistorical(mode); + const chart = generateAsciichart(historicalData.chart).split("\n"); - // generate historical graph; - const chart = generateAsciichart(Object.values(data)).split("\n"); - - // prettier-ignore - rows.push(`${ mode.charAt(0).toUpperCase() + mode.slice(1) } from ${firstDate} to ${lastDate}`.magenta) + rows.push(historicalData.date.magenta); rows = rows.concat(chart); - let response = addFooterAndGenerateChart( + let response = generateOutput( "Global Historical Chart", updated, - rows + rows, + quiet ); return response; @@ -164,16 +78,21 @@ export const globalHistory: ( * Shows Cases, Deaths, Recovered, Active, Cases/Million * Today Cases, Today Deaths, Critical, Mortality %, Recovery in a chart * @param country country code or country name that the user wants to query + * @param quiet tells the response to be in quiet mode or not */ export const informationPerCountry: ( - country: string -) => Promise = async (country) => { - let [updated, _, countryName, rows] = await getCountryInfo(country); + country: string, + quiet: boolean +) => Promise = async (country, quiet) => { + // prettier-ignore + let [updated, _, countryName, rows] = (await getCountryInfo(country)) as [ + number, string, string, (string[] | string)[]]; - let response = addFooterAndGenerateChart( + let response = generateOutput( `${countryName} Update`, updated, - rows + rows, + quiet ); // return response; @@ -183,15 +102,17 @@ export const informationPerCountry: ( /** * globalInformation tracks the info of all countries * Shows Cases, Deaths, Recovered, Mortality %, Recovered% in a chart + * @param quiet tells the response to be in quiet mode or not */ -export const globalInformation: () => Promise = async () => { - const [updated, rowsOfData] = await getAllInfo(); +export const globalInformation: (quiet: boolean) => Promise = async ( + quiet +) => { + const [updated, rowsOfData] = (await getAllInfo()) as [ + number, + (string[] | string)[] + ]; - let response = addFooterAndGenerateChart( - "Global Update", - updated, - rowsOfData - ); + let response = generateOutput("Global Update", updated, rowsOfData, quiet); return response; }; diff --git a/src/utils/plainHandlers.ts b/src/utils/plainHandlers.ts new file mode 100644 index 0000000..27a7c69 --- /dev/null +++ b/src/utils/plainHandlers.ts @@ -0,0 +1,98 @@ +import { generateAsciichart } from "./generateAsciichart"; +import { generatePlainOutput } from "./generatePlainOutput"; +import { + getAllInfo, + getCountryInfo, + getHistorical, + PlainData, +} from "./getInformation"; + +/** + * globalHistory shows a tablechart of the cases of all the countries + * Shows Cases, Deaths, Recovered, Active, Cases/Million + * and a graph of a country's cases + * @param mode Mode that the user wants to query, must be: "cases" | "deaths" | "recoveries" + * @param quiet tells the response to be in quiet mode or not + */ +export const globalHistoryPlain: ( + mode: "cases" | "deaths" | "recovered", + quiet: boolean +) => Promise = async (mode, quiet) => { + // Get summary info + const info = (await getAllInfo(true)) as PlainData; + + // Get data from API + const historicalData = await getHistorical(mode); + + // Generate historical graph + const chart = generateAsciichart(historicalData.chart, true, 7); + + return generatePlainOutput(info, `Global Historical Chart`, quiet, [ + historicalData.date, + chart, + ]); +}; + +/** + * historyPerCountry shows a tablechart of the of a country + * Shows Cases, Deaths, Recovered, Active, Cases/Million + * Today Cases, Today Deaths, Critical, Mortality %, Recovery in a chart + * @param country country code or country name that the user wants to query + * @param mode Mode that the user wants to query, must be: "cases" | "deaths" | "recoveries" + * @param quiet tells the response to be in quiet mode or not + */ + +export const historyPerCountryPlain: ( + country: string, + mode: "cases" | "deaths" | "recovered", + quiet: boolean +) => Promise = async (country, mode, quiet) => { + // Get summary info about a country + const info = (await getCountryInfo(country, true)) as PlainData; + + const historicalData = await getHistorical( + mode, + info.metainfo.countryName as string + ); + + // Generate historical graph + const chart = generateAsciichart(historicalData.chart, true, 7); + + return generatePlainOutput( + info, + `${info.metainfo.countryName} Chart`, + quiet, + [historicalData.date, chart] + ); +}; + +/** + * informationPerCountry tracks the info of a country + * Shows Cases, Deaths, Recovered, Active, Cases/Million + * Today Cases, Today Deaths, Critical, Mortality %, Recovery in a chart + * @param country country code or country name that the user wants to query + * @param quiet tells the response to be in quiet mode or not + */ +export const informationPerCountryPlain: ( + country: string, + quiet: boolean +) => Promise = async (country, quiet) => { + const info = (await getCountryInfo(country, true)) as PlainData; + return generatePlainOutput( + info, + `${info.metainfo.countryName} Update`, + quiet + ); +}; + +/** + * globalInformation tracks the info of all countries + * Shows Cases, Deaths, Recovered, Mortality %, Recovered% in a chart + * @param quiet tells the response to be in quiet mode or not + */ +export const globalInformationPlain: ( + quiet: boolean +) => Promise = async (quiet) => { + const info = (await getAllInfo(true)) as PlainData; + return generatePlainOutput(info, "Global Update", quiet); +};