diff --git a/README.md b/README.md index 66278a4..185578b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ | GET
`application/json` | `/balance` | Antelope Tokens balance changes | | GET
`application/json` | `/transfers` | Antelope Tokens transfers | | GET
`text/plain` | `/health` | Performs health checks and checks if the database is accessible | -| GET
`text/plain` | `/metrics` | [Prometheus](https://prometheus.io/) metrics | +| GET
`application/json` | `/head` | Information about the current head block in the database | +| GET
`text/plain` | `/metrics` | [Prometheus](https://prometheus.io/) metrics for the API | | GET
`application/json` | `/openapi` | [OpenAPI](https://www.openapis.org/) specification | | GET
`application/json` | `/version` | API version and commit hash | diff --git a/src/fetch/GET.ts b/src/fetch/GET.ts index 26dbc10..40f78dd 100644 --- a/src/fetch/GET.ts +++ b/src/fetch/GET.ts @@ -1,6 +1,7 @@ import { registry } from "../prometheus.js"; import openapi from "./openapi.js"; import health from "./health.js"; +import head from "./head.js"; import balance from "./balance.js"; import supply from "./supply.js"; import * as prometheus from "../prometheus.js"; @@ -26,6 +27,7 @@ export default async function (req: Request) { if (pathname === "/version") return toJSON({ version: APP_VERSION.split('+')[0], commit: APP_VERSION.split('+')[1] }); // Token endpoints + if (pathname === "/head") return head(req); if (pathname === "/supply") return supply(req); if (pathname === "/balance") return balance(req); if (pathname === "/transfers") return transfers(req); diff --git a/src/fetch/head.ts b/src/fetch/head.ts new file mode 100644 index 0000000..0a56bac --- /dev/null +++ b/src/fetch/head.ts @@ -0,0 +1,10 @@ +import { addMetadata, toJSON } from "./utils.js"; +import { makeQuery } from "../clickhouse/makeQuery.js"; + +export default async function (req: Request) { + return toJSON( + addMetadata( + await makeQuery("SELECT block_num FROM cursors ORDER BY block_num DESC LIMIT 1") + ) + ); +} \ No newline at end of file diff --git a/src/fetch/openapi.ts b/src/fetch/openapi.ts index a3623d6..2881d36 100644 --- a/src/fetch/openapi.ts +++ b/src/fetch/openapi.ts @@ -17,6 +17,17 @@ const timestampExamplesArrayFilter = ["greater_or_equals_by_timestamp", "greater const blockExamplesArrayFilter = ["greater_or_equals_by_block", "greater_by_block", "less_or_equals_by_block", "less_by_block"]; const amountExamplesArrayFilter = ["amount_greater_or_equals", "amount_greater", "amount_less_or_equals", "amount_less"]; +const head_example = addMetadata({ + meta: [], + data: [{ block_num: "107439534" }], + rows: 0, + rows_before_limit_at_least: 0, + statistics: { + elapsed: 0.00132, + rows_read: 4, + bytes_read: 32 + } +}); const supply_example = await makeQuery(getTotalSupply(new URLSearchParams({ limit: "1" }), true)).then(res => addMetadata(res, 1, 1)); const balance_example = await makeQuery(getBalanceChanges(new URLSearchParams({ limit: "2" }), true)).then(res => addMetadata(res, 2, 1)); const transfers_example = await makeQuery(getTransfers(new URLSearchParams({ limit: "5" }), true)).then(res => addMetadata(res, 5, 1)); @@ -164,11 +175,18 @@ export default new OpenApiBuilder() responses: { 200: { description: "OK", content: { "text/plain": { example: "OK" } } } }, }, }) + .addPath("/head", { + get: { + tags: [TAGS.MONITORING], + summary: "Information about the current head block in the database", + responses: { 200: { description: "Information about the current head block in the database", content: { "application/json": { example: head_example } } } }, + }, + }) .addPath("/metrics", { get: { tags: [TAGS.MONITORING], - summary: "Prometheus metrics", - responses: { 200: { description: "Prometheus metrics", content: { "text/plain": { example: await registry.metrics(), schema: { type: "string" } } } } }, + summary: "Prometheus metrics for the API", + responses: { 200: { description: "Prometheus metrics for the API", content: { "text/plain": { example: await registry.metrics(), schema: { type: "string" } } } } }, }, }) .addPath("/openapi", { diff --git a/src/fetch/utils.spec.ts b/src/fetch/utils.spec.ts index d4eed9f..220ac6d 100644 --- a/src/fetch/utils.spec.ts +++ b/src/fetch/utils.spec.ts @@ -2,20 +2,20 @@ import { expect, test } from "bun:test"; import { addMetadata } from "./utils.js"; import { Query } from "../clickhouse/makeQuery.js"; -test("addMetadata pagination", () => { - const limit = 5; - const mock_query_reponse: Query = { - meta: [], - data: Array(limit), - rows: limit, - rows_before_limit_at_least: 5*limit, // Simulate query with more total results than the query limit making pagination relevant - statistics: { - elapsed: 0, - rows_read: 0, - bytes_read: 0, - } - }; +const limit = 5; +const mock_query_reponse: Query = { + meta: [], + data: Array(limit), + rows: limit, + rows_before_limit_at_least: 5*limit, // Simulate query with more total results than the query limit making pagination relevant + statistics: { + elapsed: 0, + rows_read: 0, + bytes_read: 0, + } +}; +test("addMetadata pagination", () => { const first_page = addMetadata(mock_query_reponse, limit, 1); expect(first_page.meta.next_page).toBe(2); expect(first_page.meta.previous_page).toBe(1); // Previous page should be set to 1 on first page @@ -35,6 +35,7 @@ test("addMetadata pagination", () => { expect(even_page.meta.total_results).toBe(5*limit); const last_page = addMetadata(mock_query_reponse, limit, 5); + // @ts-ignore expect(last_page.meta.next_page).toBe(last_page.meta.total_pages); // Next page should be capped to total_pages on last page expect(last_page.meta.previous_page).toBe(4); expect(last_page.meta.total_pages).toBe(5); @@ -42,4 +43,13 @@ test("addMetadata pagination", () => { // TODO: Expect error message on beyond last page // const beyond_last_page = addMetadata(mock_query_reponse.data, mock_query_reponse.rows_before_limit_at_least, limit, 6); +}); + +test("addMetadata no pagination", () => { + const no_pagination = addMetadata(mock_query_reponse); + + expect(no_pagination.meta.next_page).toBeUndefined(); + expect(no_pagination.meta.previous_page).toBeUndefined(); + expect(no_pagination.meta.total_pages).toBeUndefined(); + expect(no_pagination.meta.total_results).toBeUndefined(); }); \ No newline at end of file diff --git a/src/fetch/utils.ts b/src/fetch/utils.ts index be97426..5195632 100644 --- a/src/fetch/utils.ts +++ b/src/fetch/utils.ts @@ -4,16 +4,24 @@ export function toJSON(data: any, status: number = 200) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } }); } -export function addMetadata(response: Query, req_limit: number, req_page: number) { +export function addMetadata(response: Query, req_limit?: number, req_page?: number) { // TODO: Catch page number greater than total_pages and return error - return { - data: response.data, - meta: { - statistics: response.statistics, - "next_page": (req_page * req_limit >= response.rows_before_limit_at_least) ? req_page : req_page + 1, - "previous_page": (req_page <= 1) ? req_page : req_page - 1, - "total_pages": Math.ceil( response.rows_before_limit_at_least / req_limit), - "total_results": response.rows_before_limit_at_least + if (typeof(req_limit) !== 'undefined' && typeof(req_page) !== 'undefined') + return { + data: response.data, + meta: { + statistics: response.statistics, + "next_page": (req_page * req_limit >= response.rows_before_limit_at_least) ? req_page : req_page + 1, + "previous_page": (req_page <= 1) ? req_page : req_page - 1, + "total_pages": Math.ceil( response.rows_before_limit_at_least / req_limit), + "total_results": response.rows_before_limit_at_least + } + } + else + return { + data: response.data, + meta: { + statistics: response.statistics, + } } - } } \ No newline at end of file