diff --git a/README.md b/README.md index 1bf403b..b897f34 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Base URL: http://stats.filspark.com/ http://stats.filspark.com/participants/monthly +- `GET /participants/accumulative/daily?from=&to=` + + http://stats.filspark.com/participants/accumulative/daily + - `GET /participants/change-rates?from=&to=` http://stats.filspark.com/participants/change-rates diff --git a/stats/lib/platform-routes.js b/stats/lib/platform-routes.js index 3bb17a0..574240e 100644 --- a/stats/lib/platform-routes.js +++ b/stats/lib/platform-routes.js @@ -7,7 +7,8 @@ import { fetchTopEarningParticipants, fetchParticipantsWithTopMeasurements, fetchDailyStationMeasurementCounts, - fetchParticipantsSummary + fetchParticipantsSummary, + fetchAccumulativeDailyParticipantCount } from './platform-stats-fetchers.js' const createRespondWithFetchFn = (pathname, searchParams, res) => (pgPool, fetchFn) => { @@ -40,6 +41,8 @@ export const handlePlatformRoutes = async (req, res, pgPools) => { await respond(pgPools.stats, fetchTopEarningParticipants) } else if (req.method === 'GET' && url === '/participants/summary') { await respondWithParticipantsSummary(res, pgPools) + } else if (req.method === 'GET' && url === '/participants/accumulative/daily') { + await respond(pgPools.evaluate, fetchAccumulativeDailyParticipantCount) } else if (req.method === 'GET' && url === '/transfers/daily') { await respond(pgPools.stats, fetchDailyRewardTransfers) } else { diff --git a/stats/lib/platform-stats-fetchers.js b/stats/lib/platform-stats-fetchers.js index 5d77456..30c96ec 100644 --- a/stats/lib/platform-stats-fetchers.js +++ b/stats/lib/platform-stats-fetchers.js @@ -98,6 +98,33 @@ export const fetchDailyRewardTransfers = async (pgPool, filter) => { return Object.values(days) } +/** + * @param {Queryable} pgPool + * @param {import('./typings.js').DateRangeFilter} filter + */ +export const fetchAccumulativeDailyParticipantCount = async (pgPool, filter) => { + const { rows } = await pgPool.query(` + WITH first_appearance AS ( + SELECT participant_id, MIN(day) as day + FROM daily_participants + GROUP BY participant_id + ), + cumulative_participants AS ( + SELECT + day, + COUNT(participant_id) OVER (ORDER BY day) AS cumulative_participants + FROM first_appearance + ) + SELECT + DISTINCT(day::TEXT), + cumulative_participants::INT as participants + FROM cumulative_participants + WHERE day >= $1 AND day <= $2 + ORDER BY day + `, [filter.from, filter.to]) + return rows +} + /** * @param {Queryable} pgPool * @param {import('./typings.js').DateRangeFilter} filter diff --git a/stats/test/platform-routes.test.js b/stats/test/platform-routes.test.js index c7c37af..bf38a74 100644 --- a/stats/test/platform-routes.test.js +++ b/stats/test/platform-routes.test.js @@ -395,6 +395,57 @@ describe('Platform Routes HTTP request handler', () => { ) }) }) + + describe('GET /participants/accumulative/daily', () => { + it('counts accumulative daily participants', async () => { + // 3 new participants, out of range + await givenDailyParticipants( + pgPools.evaluate, + '1999-01-01', + ['0x10', '0x20', '0x30'] + ) + // 3 new participants, 1 old participant -> 6 + await givenDailyParticipants( + pgPools.evaluate, + '2000-01-01', + ['0x1', '0x2', '0x3', '0x10'] + ) + // 0 new participants, 2 old participants + await givenDailyParticipants( + pgPools.evaluate, + '2000-01-02', + ['0x1', '0x2'] + ) + // 1 new participant, 1 old participant -> 7 + await givenDailyParticipants( + pgPools.evaluate, + '2000-01-03', + ['0x1', '0x4'] + ) + // 1 new participant, out of range + await givenDailyParticipants( + pgPools.evaluate, + '2000-01-04', + ['0x5'] + ) + + const res = await fetch( + new URL('/participants/accumulative/daily?from=2000-01-01&to=2000-01-03', baseUrl), { + redirect: 'manual' + } + ) + await assertResponseStatus(res, 200) + const daily = await res.json() + assert.deepStrictEqual(daily, [ + { day: '2000-01-01', participants: 6 }, + { day: '2000-01-03', participants: 7 } + ]) + assert.strictEqual( + res.headers.get('cache-control'), + 'public, max-age=31536000, immutable' + ) + }) + }) }) const givenDailyMeasurementsSummary = async (pgPoolEvaluate, summaryData) => {