diff --git a/api/index.js b/api/index.js index 0a25e372..30834fcc 100644 --- a/api/index.js +++ b/api/index.js @@ -1,5 +1,10 @@ require("dotenv").config(); -const { renderError, parseBoolean } = require("../src/utils"); +const { + renderError, + parseBoolean, + clampValue, + CONSTANTS, +} = require("../src/utils"); const fetchStats = require("../src/fetchStats"); const renderStatsCard = require("../src/renderStatsCard"); @@ -17,10 +22,10 @@ module.exports = async (req, res) => { text_color, bg_color, theme, + cache_seconds, } = req.query; let stats; - res.setHeader("Cache-Control", "public, max-age=1800"); res.setHeader("Content-Type", "image/svg+xml"); try { @@ -29,6 +34,14 @@ module.exports = async (req, res) => { return res.send(renderError(err.message)); } + const cacheSeconds = clampValue( + parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10), + CONSTANTS.THIRTY_MINUTES, + CONSTANTS.ONE_DAY + ); + + res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); + res.send( renderStatsCard(stats, { hide: JSON.parse(hide || "[]"), diff --git a/api/pin.js b/api/pin.js index bebe529a..ad24b05b 100644 --- a/api/pin.js +++ b/api/pin.js @@ -1,5 +1,10 @@ require("dotenv").config(); -const { renderError, parseBoolean } = require("../src/utils"); +const { + renderError, + parseBoolean, + clampValue, + CONSTANTS, +} = require("../src/utils"); const fetchRepo = require("../src/fetchRepo"); const renderRepoCard = require("../src/renderRepoCard"); @@ -13,11 +18,11 @@ module.exports = async (req, res) => { bg_color, theme, show_owner, + cache_seconds, } = req.query; let repoData; - res.setHeader("Cache-Control", "public, max-age=1800"); res.setHeader("Content-Type", "image/svg+xml"); try { @@ -27,6 +32,27 @@ module.exports = async (req, res) => { return res.send(renderError(err.message)); } + let cacheSeconds = clampValue( + parseInt(cache_seconds || CONSTANTS.THIRTY_MINUTES, 10), + CONSTANTS.THIRTY_MINUTES, + CONSTANTS.ONE_DAY + ); + + /* + if star count & fork count is over 1k then we are kFormating the text + and if both are zero we are not showing the stats + so we can just make the cache longer, since there is no need to frequent updates + */ + const stars = repoData.stargazers.totalCount; + const forks = repoData.forkCount; + const isBothOver1K = stars > 1000 && forks > 1000; + const isBothUnder1 = stars < 1 && forks < 1; + if (!cache_seconds && (isBothOver1K || isBothUnder1)) { + cacheSeconds = CONSTANTS.TWO_HOURS; + } + + res.setHeader("Cache-Control", `public, max-age=${cacheSeconds}`); + res.send( renderRepoCard(repoData, { title_color, diff --git a/src/renderRepoCard.js b/src/renderRepoCard.js index 1c913397..50b3004f 100644 --- a/src/renderRepoCard.js +++ b/src/renderRepoCard.js @@ -75,7 +75,7 @@ const renderRepoCard = (repo, options = {}) => { `; const svgForks = - totalForks > 0 && + forkCount > 0 && ` ${icons.fork} diff --git a/src/utils.js b/src/utils.js index ca920e06..fd51eeda 100644 --- a/src/utils.js +++ b/src/utils.js @@ -44,6 +44,10 @@ function parseBoolean(value) { } } +function clampValue(number, min, max) { + return Math.max(min, Math.min(number, max)); +} + function fallbackColor(color, fallbackColor) { return (isValidHexColor(color) && `#${color}`) || fallbackColor; } @@ -112,6 +116,12 @@ function getCardColors({ return { titleColor, iconColor, textColor, bgColor }; } +const CONSTANTS = { + THIRTY_MINUTES: 1800, + TWO_HOURS: 7200, + ONE_DAY: 86400, +}; + module.exports = { renderError, kFormatter, @@ -122,4 +132,6 @@ module.exports = { fallbackColor, FlexLayout, getCardColors, + clampValue, + CONSTANTS, }; diff --git a/tests/api.test.js b/tests/api.test.js index d6c4b4b1..4f0be8fb 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -3,7 +3,7 @@ const axios = require("axios"); const MockAdapter = require("axios-mock-adapter"); const api = require("../api/index"); const renderStatsCard = require("../src/renderStatsCard"); -const { renderError } = require("../src/utils"); +const { renderError, CONSTANTS } = require("../src/utils"); const calculateRank = require("../src/calculateRank"); const stats = { @@ -55,22 +55,29 @@ const error = { const mock = new MockAdapter(axios); +const faker = (query, data) => { + const req = { + query: { + username: "anuraghazra", + ...query, + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock.onPost("https://api.github.com/graphql").reply(200, data); + + return { req, res }; +}; + afterEach(() => { mock.reset(); }); describe("Test /api/", () => { it("should test the request", async () => { - const req = { - query: { - username: "anuraghazra", - }, - }; - const res = { - setHeader: jest.fn(), - send: jest.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data); + const { req, res } = faker({}, data); await api(req, res); @@ -79,16 +86,7 @@ describe("Test /api/", () => { }); it("should render error card on error", async () => { - const req = { - query: { - username: "anuraghazra", - }, - }; - const res = { - setHeader: jest.fn(), - send: jest.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, error); + const { req, res } = faker({}, error); await api(req, res); @@ -97,8 +95,8 @@ describe("Test /api/", () => { }); it("should get the query options", async () => { - const req = { - query: { + const { req, res } = faker( + { username: "anuraghazra", hide: `["issues","prs","contribs"]`, show_icons: true, @@ -109,12 +107,8 @@ describe("Test /api/", () => { text_color: "fff", bg_color: "fff", }, - }; - const res = { - setHeader: jest.fn(), - send: jest.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data); + data + ); await api(req, res); @@ -132,4 +126,59 @@ describe("Test /api/", () => { }) ); }); + + it("should have proper cache", async () => { + const { req, res } = faker({}, data); + mock.onPost("https://api.github.com/graphql").reply(200, data); + + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`], + ]); + }); + + it("should set proper cache", async () => { + const { req, res } = faker({ cache_seconds: 2000 }, data); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + ["Cache-Control", `public, max-age=${2000}`], + ]); + }); + + it("should set proper cache with clamped values", async () => { + { + let { req, res } = faker({ cache_seconds: 200000 }, data); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + ["Cache-Control", `public, max-age=${CONSTANTS.ONE_DAY}`], + ]); + } + + // note i'm using block scoped vars + { + let { req, res } = faker({ cache_seconds: 0 }, data); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`], + ]); + } + + { + let { req, res } = faker({ cache_seconds: -10000 }, data); + await api(req, res); + + expect(res.setHeader.mock.calls).toEqual([ + ["Content-Type", "image/svg+xml"], + ["Cache-Control", `public, max-age=${CONSTANTS.THIRTY_MINUTES}`], + ]); + } + }); });