diff --git a/README.md b/README.md index 1d539d53..15aba9be 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,32 @@ Number of deals grouped by miner IDs. } ``` +### `GET /allocator/:id/deals/eligible/summary` + +Parameters: +- `allocatorId` - an allocator id like `f03015751` + +Response: + +Number of deals grouped by miner 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 35cd578b..ae6750cc 100644 --- a/api/index.js +++ b/api/index.js @@ -32,6 +32,8 @@ const handler = async (req, res, client, domain) => { 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 { @@ -360,6 +362,33 @@ const getSummaryOfEligibleDealsForClient = async (_req, res, client, clientId) = 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 af650899..a5f639b9 100644 --- a/api/test/test.js +++ b/api/test/test.js @@ -662,6 +662,15 @@ describe('Routes', () => { ('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', () => { @@ -723,5 +732,34 @@ describe('Routes', () => { }) }) }) + + 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: [] + }) + }) + }) }) })