From 8cac0f947406bf9475cd512398fff1962e761d07 Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Wed, 14 Jul 2021 17:11:22 +0100 Subject: [PATCH] feat: GET /status/:cid to check pin & deal status (#82) - add **Unauthenticated** enpoint for checking pin and deal status by CID. - adds mock and fixture for testing. - only show pins and deals that are queued or active. **Example reponse** `GET /status/testcid` ```json { "cid": "testcid", "dagSize": 101, "pins": [{ "peerId": "12D3KooWR1Js", "peerName": "who?", "region": "where?", "status": "Pinned" }], "deals": [{ "dealId": 12345, "miner": "f99", "status": "Active", "activation": "", "pieceCid": "baga", "dataCid": "bafy", "dataModelSelector": "Links/0/Links" }] } ``` Fixes #78 License: (Apache-2.0 AND MIT) Signed-off-by: Oli Evans --- packages/api/src/index.js | 6 +- packages/api/src/status.js | 103 ++++++++++++++++++ packages/api/src/utils/json-response.js | 4 + .../find-content-by-cid-no-batch.json | 20 ++++ .../fixtures/find-content-by-cid-no-deal.json | 29 +++++ .../fixtures/find-content-by-cid-unknown.json | 13 +++ .../test/fixtures/find-content-by-cid.json | 34 ++++++ packages/api/test/fixtures/status.json | 19 ++++ packages/api/test/mocks/db/post_graphql.js | 13 +++ packages/api/test/status.spec.js | 79 ++++++++++++++ 10 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/status.js create mode 100644 packages/api/test/fixtures/find-content-by-cid-no-batch.json create mode 100644 packages/api/test/fixtures/find-content-by-cid-no-deal.json create mode 100644 packages/api/test/fixtures/find-content-by-cid-unknown.json create mode 100644 packages/api/test/fixtures/find-content-by-cid.json create mode 100644 packages/api/test/fixtures/status.json create mode 100644 packages/api/test/status.spec.js diff --git a/packages/api/src/index.js b/packages/api/src/index.js index 5372eeefa4..ccd52ee01f 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -2,9 +2,10 @@ import { Router } from 'itty-router' import { withCorsHeaders, corsOptions } from './cors.js' import { envAll } from './env.js' +import { statusGet } from './status.js' import { carHead, carGet, carPut, carPost } from './car.js' import { userLoginPost, userTokensPost, userTokensGet, userTokensDelete, userUploadsGet, userUploadsDelete, withAuth } from './user.js' -import { JSONResponse } from './utils/json-response.js' +import { JSONResponse, notFound } from './utils/json-response.js' const router = Router() @@ -14,6 +15,7 @@ router.get('/car/:cid', withCorsHeaders(carGet)) router.head('/car/:cid', withCorsHeaders(carHead)) router.put('/car/:cid', withCorsHeaders(withAuth(carPut))) router.post('/car', withCorsHeaders(withAuth(carPost))) +router.get('/status/:cid', withCorsHeaders(statusGet)) router.post('/user/login', withCorsHeaders(userLoginPost)) router.get('/user/tokens', withCorsHeaders(withAuth(userTokensGet))) router.post('/user/tokens', withCorsHeaders(withAuth(userTokensPost))) @@ -41,7 +43,7 @@ router.get('/', () => { ) }) -router.all('*', () => new JSONResponse({ message: 'Not Found' }, { status: 404 })) +router.all('*', () => notFound()) function serverError (error) { console.error(error.stack) diff --git a/packages/api/src/status.js b/packages/api/src/status.js new file mode 100644 index 0000000000..39d7a0df4b --- /dev/null +++ b/packages/api/src/status.js @@ -0,0 +1,103 @@ +import { gql } from '@web3-storage/db' +import { JSONResponse, notFound } from './utils/json-response.js' + +const DEAL_STATUS = new Set([ + 'Queued', + 'Published', + 'Active' +]) + +const PIN_STATUS = new Set([ + 'Pinned', + 'Pinning', + 'PinQueued' +]) + +/** + * Returns pin and deal status info for a given CID. + * + * @see {@link ../test/fixtures/status.json|Example response} + * @param {Request} request + * @param {import('./env').Env} env + * @returns {Response} + */ +export async function statusGet (request, env) { + const cid = request.params.cid + const result = await env.db.query( + gql`query FindContentByCid($cid: String!) { + findContentByCid(cid: $cid) { + dagSize + batchEntries { + data { + dataModelSelector + batch { + cid + pieceCid + deals { + data { + miner + chainDealId + activation + status + } + } + } + } + } + pins { + data { + status + location { + peerId + peerName + region + } + } + } + } + } + `, { cid }) + + const { findContentByCid: raw } = result.data + const { dagSize } = raw + + if (raw.pins.data.length === 0 && raw.batchEntries.data.length === 0) { + return notFound() + } + + const pins = raw.pins.data + .filter(({ status }) => PIN_STATUS.has(status)) + .map(({ status, location }) => ({ status, ...location })) + + const deals = raw.batchEntries.data.map(({ dataModelSelector, batch }) => { + const { pieceCid, cid: dataCid, deals } = batch + if (deals.data.length === 0) { + return [{ + status: 'Queued', + pieceCid, + dataCid, + dataModelSelector + }] + } + return deals.data + .filter(({ status }) => DEAL_STATUS.has(status)) + .map(({ chainDealId: dealId, miner, activation, status }) => ({ + dealId, + miner, + status, + activation, + pieceCid, + dataCid, + dataModelSelector + })) + }).reduce((a, b) => a.concat(b), []) // flatten array of arrays. + + const status = { + cid, + dagSize, + pins, + deals + } + + return new JSONResponse(status) +} diff --git a/packages/api/src/utils/json-response.js b/packages/api/src/utils/json-response.js index 7f1e1e7fd3..f4a51c9e2f 100644 --- a/packages/api/src/utils/json-response.js +++ b/packages/api/src/utils/json-response.js @@ -10,3 +10,7 @@ export class JSONResponse extends Response { super(JSON.stringify(body), init) } } + +export function notFound (message = 'Not Found') { + return new JSONResponse({ message }, { status: 404 }) +} diff --git a/packages/api/test/fixtures/find-content-by-cid-no-batch.json b/packages/api/test/fixtures/find-content-by-cid-no-batch.json new file mode 100644 index 0000000000..671ab3d2e4 --- /dev/null +++ b/packages/api/test/fixtures/find-content-by-cid-no-batch.json @@ -0,0 +1,20 @@ +{ + "data": { + "findContentByCid": { + "dagSize": 101, + "batchEntries": { + "data": [] + }, + "pins": { + "data": [{ + "status": "Pinned", + "location": { + "peerId": "12D3KooWR1Js", + "peerName": "who?", + "region": "where?" + } + }] + } + } + } +} diff --git a/packages/api/test/fixtures/find-content-by-cid-no-deal.json b/packages/api/test/fixtures/find-content-by-cid-no-deal.json new file mode 100644 index 0000000000..7983802353 --- /dev/null +++ b/packages/api/test/fixtures/find-content-by-cid-no-deal.json @@ -0,0 +1,29 @@ +{ + "data": { + "findContentByCid": { + "dagSize": 101, + "batchEntries": { + "data": [{ + "dataModelSelector": "Links/0/Links", + "batch": { + "cid": "bafy", + "pieceCid": "baga", + "deals": { + "data" : [] + } + } + }] + }, + "pins": { + "data": [{ + "status": "Pinned", + "location": { + "peerId": "12D3KooWR1Js", + "peerName": "who?", + "region": "where?" + } + }] + } + } + } +} diff --git a/packages/api/test/fixtures/find-content-by-cid-unknown.json b/packages/api/test/fixtures/find-content-by-cid-unknown.json new file mode 100644 index 0000000000..d01ab3f8ad --- /dev/null +++ b/packages/api/test/fixtures/find-content-by-cid-unknown.json @@ -0,0 +1,13 @@ +{ + "data": { + "findContentByCid": { + "dagSize": null, + "batchEntries": { + "data": [] + }, + "pins": { + "data": [] + } + } + } +} diff --git a/packages/api/test/fixtures/find-content-by-cid.json b/packages/api/test/fixtures/find-content-by-cid.json new file mode 100644 index 0000000000..9abf1c2fc5 --- /dev/null +++ b/packages/api/test/fixtures/find-content-by-cid.json @@ -0,0 +1,34 @@ +{ + "data": { + "findContentByCid": { + "dagSize": 101, + "batchEntries": { + "data": [{ + "dataModelSelector": "Links/0/Links", + "batch": { + "cid": "bafy", + "pieceCid": "baga", + "deals": { + "data" : [ { + "miner": "f99", + "chainDealId": 12345, + "activation": "", + "status": "Active" + }] + } + } + }] + }, + "pins": { + "data": [{ + "status": "Pinned", + "location": { + "peerId": "12D3KooWR1Js", + "peerName": "who?", + "region": "where?" + } + }] + } + } + } +} diff --git a/packages/api/test/fixtures/status.json b/packages/api/test/fixtures/status.json new file mode 100644 index 0000000000..24c4ebebc9 --- /dev/null +++ b/packages/api/test/fixtures/status.json @@ -0,0 +1,19 @@ +{ + "cid": "testcid", + "dagSize": 101, + "pins": [{ + "peerId": "12D3KooWR1Js", + "peerName": "who?", + "region": "where?", + "status": "Pinned" + }], + "deals": [{ + "dealId": 12345, + "miner": "f99", + "status": "Active", + "activation": "", + "pieceCid": "baga", + "dataCid": "bafy", + "dataModelSelector": "Links/0/Links" + }] +} diff --git a/packages/api/test/mocks/db/post_graphql.js b/packages/api/test/mocks/db/post_graphql.js index e481cef8fd..3cb3fa1e3e 100644 --- a/packages/api/test/mocks/db/post_graphql.js +++ b/packages/api/test/mocks/db/post_graphql.js @@ -50,6 +50,19 @@ module.exports = ({ body }) => { return gqlOkResponse({ deleteUserUpload: { _id: 'test-delete-user-upload' } }) } + if (body.query.includes('findContentByCid')) { + if (body.variables.cid === 'unknown') { + return gqlOkResponse(require('../../fixtures/find-content-by-cid-unknown.json')) + } + if (body.variables.cid === 'nobatch') { + return gqlOkResponse(require('../../fixtures/find-content-by-cid-no-batch.json')) + } + if (body.variables.cid === 'nodeal') { + return gqlOkResponse(require('../../fixtures/find-content-by-cid-no-deal.json')) + } + return gqlOkResponse(require('../../fixtures/find-content-by-cid.json')) + } + return gqlResponse(400, { errors: [{ message: `unexpected query: ${body.query}` }] }) diff --git a/packages/api/test/status.spec.js b/packages/api/test/status.spec.js new file mode 100644 index 0000000000..51f83e050f --- /dev/null +++ b/packages/api/test/status.spec.js @@ -0,0 +1,79 @@ +/* eslint-env mocha, browser */ +import assert from 'assert' +import { endpoint } from './scripts/constants.js' + +describe('GET /status/:cid', () => { + it('get pin and deal status', async () => { + const cid = 'testcid' + const res = await fetch(new URL(`status/${cid}`, endpoint)) + assert(res.ok) + const json = await res.json() + assert.deepStrictEqual(json, { + cid: 'testcid', + dagSize: 101, + pins: [{ + peerId: '12D3KooWR1Js', + peerName: 'who?', + region: 'where?', + status: 'Pinned' + }], + deals: [{ + dealId: 12345, + miner: 'f99', + status: 'Active', + activation: '', + pieceCid: 'baga', + dataCid: 'bafy', + dataModelSelector: 'Links/0/Links' + }] + }) + }) + + it('get shows initial queued deal', async () => { + const cid = 'nodeal' + const res = await fetch(new URL(`status/${cid}`, endpoint)) + assert(res.ok) + const json = await res.json() + assert.deepStrictEqual(json, { + cid, + dagSize: 101, + pins: [{ + peerId: '12D3KooWR1Js', + peerName: 'who?', + region: 'where?', + status: 'Pinned' + }], + deals: [{ + status: 'Queued', + pieceCid: 'baga', + dataCid: 'bafy', + dataModelSelector: 'Links/0/Links' + }] + }) + }) + + it('get shows no deals before batch is ready', async () => { + const cid = 'nobatch' + const res = await fetch(new URL(`status/${cid}`, endpoint)) + assert(res.ok) + const json = await res.json() + assert.deepStrictEqual(json, { + cid, + dagSize: 101, + pins: [{ + peerId: '12D3KooWR1Js', + peerName: 'who?', + region: 'where?', + status: 'Pinned' + }], + deals: [] + }) + }) + + it('get 404 for unknown cid', async () => { + const cid = 'unknown' + const res = await fetch(new URL(`status/${cid}`, endpoint)) + assert(!res.ok) + assert.strictEqual(res.status, 404) + }) +})