diff --git a/README.md b/README.md index 8a98bc9d..a0b7fb23 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,66 @@ Response: OK ``` +### `GET /miner/:minerId/deals/eligible/summary` + +Parameters: +- `minerId` - a miner id like `f0814049` + +Response: + +Number of deals grouped by client IDs. + +```json +{ + "minerId": "f0814049", + "dealCount": 13878, + "clients": [ + { "clientId": "f02516933", "dealCount": 6880 }, + { "clientId": "f02833886", "dealCount": 3126 } + ] +} +``` + +### `GET /client/:clientId/deals/eligible/summary` + +Parameters: +- `clientId` - a client id like `f0215074` + +Response: + +Number of deals grouped by miner IDs. + +```json +{ + "clientId": "f0215074", + "dealCount": 38977, + "providers": [ + { "minerId": "f01975316", "dealCount": 6810 }, + { "minerId": "f01975326", "dealCount": 6810 } + ] +} +``` + +### `GET /allocator/:allocatorId/deals/eligible/summary` + +Parameters: +- `allocatorId` - an allocator id like `f03015751` + +Response: + +Number of deals grouped by client IDs. + +```json +{ + "allocatorId": "f03015751", + "dealCount": 4088, + "clients": [ + { "clientId": "f03144229", "dealCount": 2488 }, + { "clientId": "f03150656", "dealCount": 1600 } + ] +} +``` + ## Development ### Database diff --git a/api/index.js b/api/index.js index 14f493a6..ae6750cc 100644 --- a/api/index.js +++ b/api/index.js @@ -28,6 +28,12 @@ const handler = async (req, res, client, domain) => { await getMeridianRoundDetails(req, res, client, segs[2], segs[3]) } else if (segs[0] === 'rounds' && req.method === 'GET') { await getRoundDetails(req, res, client, segs[1]) + } else if (segs[0] === 'miner' && segs[1] && segs[2] === 'deals' && segs[3] === 'eligible' && segs[4] === 'summary' && req.method === 'GET') { + await getSummaryOfEligibleDealsForMiner(req, res, client, segs[1]) + } else if (segs[0] === 'client' && segs[1] && segs[2] === 'deals' && segs[3] === 'eligible' && segs[4] === 'summary' && req.method === 'GET') { + await getSummaryOfEligibleDealsForClient(req, res, client, segs[1]) + } else if (segs[0] === 'allocator' && segs[1] && segs[2] === 'deals' && segs[3] === 'eligible' && segs[4] === 'summary' && req.method === 'GET') { + await getSummaryOfEligibleDealsForAllocator(req, res, client, segs[1]) } else if (segs[0] === 'inspect-request' && req.method === 'GET') { await inspectRequest(req, res) } else { @@ -304,6 +310,85 @@ const redirect = (res, location) => { res.end() } +const getSummaryOfEligibleDealsForMiner = async (_req, res, client, minerId) => { + /** @type {{rows: {client_id: string; deal_count: number}[]}} */ + const { rows } = await client.query(` + SELECT client_id, COUNT(cid)::INTEGER as deal_count FROM retrievable_deals + WHERE miner_id = $1 AND expires_at > now() + GROUP BY client_id + ORDER BY deal_count DESC, client_id ASC + `, [ + minerId + ]) + + // Cache the response for 6 hours + res.setHeader('cache-control', `max-age=${6 * 3600}`) + + const body = { + minerId, + dealCount: rows.reduce((sum, row) => sum + row.deal_count, 0), + clients: + rows.map( + // eslint-disable-next-line camelcase + ({ client_id, deal_count }) => ({ clientId: client_id, dealCount: deal_count }) + ) + } + + json(res, body) +} + +const getSummaryOfEligibleDealsForClient = async (_req, res, client, clientId) => { + /** @type {{rows: {miner_id: string; deal_count: number}[]}} */ + const { rows } = await client.query(` + SELECT miner_id, COUNT(cid)::INTEGER as deal_count FROM retrievable_deals + WHERE client_id = $1 AND expires_at > now() + GROUP BY miner_id + ORDER BY deal_count DESC, miner_id ASC + `, [ + clientId + ]) + + // Cache the response for 6 hours + res.setHeader('cache-control', `max-age=${6 * 3600}`) + + const body = { + clientId, + dealCount: rows.reduce((sum, row) => sum + row.deal_count, 0), + providers: rows.map( + // eslint-disable-next-line camelcase + ({ miner_id, deal_count }) => ({ minerId: miner_id, dealCount: deal_count }) + ) + } + json(res, body) +} + +const getSummaryOfEligibleDealsForAllocator = async (_req, res, client, allocatorId) => { + /** @type {{rows: {client_id: string; deal_count: number}[]}} */ + const { rows } = await client.query(` + SELECT ac.client_id, COUNT(cid)::INTEGER as deal_count + FROM allocator_clients ac + LEFT JOIN retrievable_deals rd ON ac.client_id = rd.client_id + WHERE ac.allocator_id = $1 AND expires_at > now() + GROUP BY ac.client_id + ORDER BY deal_count DESC, ac.client_id ASC + `, [ + allocatorId + ]) + + // Cache the response for 6 hours + res.setHeader('cache-control', `max-age=${6 * 3600}`) + + const body = { + allocatorId, + dealCount: rows.reduce((sum, row) => sum + row.deal_count, 0), + clients: rows.map( + // eslint-disable-next-line camelcase + ({ client_id, deal_count }) => ({ clientId: client_id, dealCount: deal_count }) + ) + } + json(res, body) +} + export const inspectRequest = async (req, res) => { await json(res, { remoteAddress: req.socket.remoteAddress, diff --git a/api/test/test.js b/api/test/test.js index 8f020f1d..a5f639b9 100644 --- a/api/test/test.js +++ b/api/test/test.js @@ -646,4 +646,120 @@ describe('Routes', () => { } }) }) + + describe('summary of eligible deals', () => { + before(async () => { + await client.query(` + INSERT INTO retrievable_deals (cid, miner_id, client_id, expires_at) + VALUES + ('bafyone', 'f0210', 'f0800', '2100-01-01'), + ('bafyone', 'f0220', 'f0800', '2100-01-01'), + ('bafytwo', 'f0220', 'f0810', '2100-01-01'), + ('bafyone', 'f0230', 'f0800', '2100-01-01'), + ('bafytwo', 'f0230', 'f0800', '2100-01-01'), + ('bafythree', 'f0230', 'f0810', '2100-01-01'), + ('bafyfour', 'f0230', 'f0820', '2100-01-01'), + ('bafyexpired', 'f0230', 'f0800', '2020-01-01') + ON CONFLICT DO NOTHING + `) + + await client.query(` + INSERT INTO allocator_clients (allocator_id, client_id) + VALUES + ('f0500', 'f0800'), + ('f0500', 'f0810'), + ('f0520', 'f0820') + ON CONFLICT DO NOTHING + `) + }) + + describe('GET /miner/{id}/deals/eligible/summary', () => { + it('returns deal counts grouped by client id', async () => { + const res = await fetch(`${spark}/miner/f0230/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + minerId: 'f0230', + dealCount: 4, + clients: [ + { clientId: 'f0800', dealCount: 2 }, + { clientId: 'f0810', dealCount: 1 }, + { clientId: 'f0820', dealCount: 1 } + ] + }) + }) + + it('returns an empty array for miners with no deals in our DB', async () => { + const res = await fetch(`${spark}/miner/f0000/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + minerId: 'f0000', + dealCount: 0, + clients: [] + }) + }) + }) + + describe('GET /client/{id}/deals/eligible/summary', () => { + it('returns deal counts grouped by miner id', async () => { + const res = await fetch(`${spark}/client/f0800/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + clientId: 'f0800', + dealCount: 4, + providers: [ + { minerId: 'f0230', dealCount: 2 }, + { minerId: 'f0210', dealCount: 1 }, + { minerId: 'f0220', dealCount: 1 } + ] + }) + }) + + it('returns an empty array for miners with no deals in our DB', async () => { + const res = await fetch(`${spark}/client/f0000/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + clientId: 'f0000', + dealCount: 0, + providers: [] + }) + }) + }) + + describe('GET /allocator/{id}/deals/eligible/summary', () => { + it('returns deal counts grouped by client id', async () => { + const res = await fetch(`${spark}/allocator/f0500/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + allocatorId: 'f0500', + dealCount: 6, + clients: [ + { clientId: 'f0800', dealCount: 4 }, + { clientId: 'f0810', dealCount: 2 } + ] + }) + }) + + it('returns an empty array for miners with no deals in our DB', async () => { + const res = await fetch(`${spark}/allocator/f0000/deals/eligible/summary`) + await assertResponseStatus(res, 200) + assert.strictEqual(res.headers.get('cache-control'), 'max-age=21600') + const body = await res.json() + assert.deepStrictEqual(body, { + allocatorId: 'f0000', + dealCount: 0, + clients: [] + }) + }) + }) + }) })