const express = require("@runkit/runkit/express-endpoint/1.0.0");
const got = require('got');
const app = express(exports);
const LruCache = require("lru-cache");
const isPromise = require('is-promise');
const humanizeMs = require('humanize-ms');
const assert = require('assert');
const cheerio = require('cheerio');
/* Declare Cache module */
class Cache {
constructor({ maxAge }) {
this.lruCache = new LruCache({
maxAge,
});
}
wrap(key, cb) {
const cache = this.lruCache;
if(cache.has(key)) {
/* do nothing */
}
else {
const result = cb(key);
cache.set(key, result);
if(isPromise(result)) {
result
.then(resolvedValue => cache.set(key, resolvedValue))
.catch(() => cache.del(key));
}
}
return cache.get(key);
}
}
/* Constants */
const ValueResearchHost = 'https://www.valueresearchonline.com/';
const OneHourMs = humanizeMs('12 hour');
const BlankValue = '--';
const NotFound = 'NA';
const Error = 'ERR';
const Fields = {
returns: 'Return Since Launch:',
launchDate: 'Launch Date:',
aum: 'Assets:',
expenseRatio: 'Expense:',
minLumpSumInv: 'Minimum Investment (₹)',
minSIPInv: 'Minimum SIP Investment (₹)',
alpha: 'Alpha',
beta: 'Beta',
sharpe: 'Sharpe',
};
/** css attributes */
const InvestmentDetailsTable = '#basic-investment-table1';
const BasicDetailsTable = '#basic-investment-table';
const RiskMeasuresTable = '#risk-measures-table';
const FundSlug = '#fund_slug';
const FundId = '#fund_id';
const HoldingDate = '#holding_date';
/** Date formats */
const AppDateFormat = 'DD-MMM-YYYY';
const HoldingDateFormat = 'YYYY-MM-DD';
const cache = new Cache({
maxAge: OneHourMs,
});
/* fetch and parse html page */
const fetchHtmlPage = (url) => cache.wrap(url, async key => {
const body = await got(key, {
prefixUrl: ValueResearchHost,
resolveBodyOnly: true,
});
const $ = cheerio.load(body);
/* assertions */
assert($(FundSlug), 'Fund slug missing');
assert($(FundId), 'Fund id missing');
assert($(InvestmentDetailsTable), 'Investment table missing');
assert($(BasicDetailsTable), 'Basic details table missing');
assert($(RiskMeasuresTable), 'Risk measure table missing');
return $;
});
/* fetch details service methods */
const trim = str => (
str
.trim()
.replace(/\s/g, ' ')
);
const compare = (a, b) => trim(a) === trim(b);
const fetchDetailsValue = async ({ id, slug, field }) => {
try {
const $ = await fetchHtmlPage(`funds/${id}/${slug}`);
const tableDataList = $(`${InvestmentDetailsTable} td, ${BasicDetailsTable} td`);
const textList = [...tableDataList]
.map(data => trim($(data).text()));
const index = textList
.findIndex(text => compare(text, field));
return index === -1
? NotFound
: textList[index + 1];
}
catch(ex) {
console.log(ex);
return Error;
}
};
const fetchRiskMeasure = async ({ id, slug, field }) => {
try {
const $ = await fetchHtmlPage(`funds/${id}/${slug}`);
const [tableHeader, ...tableRows] = $(`${RiskMeasuresTable} tr`);
const headerIndex = [...$('th', tableHeader)]
.map(header => trim($(header).text()))
.findIndex(innerText => compare(innerText, field))
const rowIndex = tableRows
.map(row => trim($('td:eq(0)', row).text()))
.findIndex(f => compare(f, 'Fund'));
if(headerIndex === -1 || rowIndex === -1)
return NotFound;
const riskMeasure = trim(
$(`td:eq(${headerIndex})`, tableRows[rowIndex]).text()
);
return riskMeasure === BlankValue
? NotFound
: riskMeasure;
}
catch(ex) {
return Error;
}
};
const fetchAsOnDate = async ({ id, slug }) => {
try {
const $ = await fetchHtmlPage(`funds/${id}/${slug}`);
const holdingDate = $(HoldingDate);
return dayjs(holdingDate, HoldingDateFormat).format(AppDateFormat);
}
catch(ex) {
console.log(ex);
return Error
}
};
/*
* End points
*/
app.get("/alpha/:id/:slug", async (req, res) => {
const { id, slug } = req.params;
console.log(id, slug);
const result = await fetchDetailsValue({
id, slug,
field: Fields.launchDate,
});
res.send(result);
});