Skip to content

Commit

Permalink
Added CORS and Swagger docs
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelpapineau committed Nov 3, 2023
1 parent 2532e29 commit c5f1367
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 2 deletions.
Binary file modified bun.lockb
Binary file not shown.
9 changes: 7 additions & 2 deletions src/fetch/GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { checkHealth } from "../health.js";
import * as prometheus from "../prometheus.js";
import * as sqlite from "../sqlite.js";
import { db } from "../../index.js";
import { toJSON } from "../http.js";
import { Server } from "bun";
import { logger } from "../logger.js";
import openapi from "./openapi.js";
import swaggerHtml from "../../swagger/index.html"
import swaggerFavicon from "../../swagger/favicon.png"
import { NotFound, toFile, toJSON, toText } from "./cors.js";

export default async function (req: Request, server: Server) {
const { pathname, searchParams} = new URL(req.url);
Expand All @@ -21,7 +24,9 @@ export default async function (req: Request, server: Server) {
return;
}

if ( pathname === "/") return new Response(banner())
//if ( pathname === "/") return new Response(banner())
if ( pathname === "/" ) return toFile(Bun.file(swaggerHtml));
if ( pathname === "/favicon.png" ) return toFile(Bun.file(swaggerFavicon));
if ( pathname === "/health") return checkHealth();
if ( pathname === "/metrics") return new Response(await prometheus.registry.metrics());
if ( pathname === "/moduleHash") return toJSON(sqlite.selectAll(db, "moduleHash"));
Expand Down
37 changes: 37 additions & 0 deletions src/fetch/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BunFile } from "bun";

export const CORS_HEADERS = new Headers({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, WWW-Authenticate",
});
export const JSON_HEADERS = new Headers({"Content-Type": "application/json"});
export const TEXT_HEADERS = new Headers({"Content-Type": "text/plain; version=0.0.4; charset=utf-8"});

export function appendHeaders(...args: Headers[]) {
const headers = new Headers(CORS_HEADERS); // CORS as default headers
for (const arg of args) {
for (const [key, value] of arg.entries()) {
headers.set(key, value);
}
}
return headers;
};

export function toJSON(body: any, status = 200, headers = new Headers()) {
const data = typeof body == "string" ? body : JSON.stringify(body, null, 2);
return new Response(data, { status, headers: appendHeaders(JSON_HEADERS, headers) });
}

export function toText(body: string, status = 200, headers = new Headers()) {
return new Response(body, { status, headers: appendHeaders(TEXT_HEADERS, headers) });
}

export function toFile(body: BunFile, status = 200, headers = new Headers()) {
const fileHeaders = new Headers({"Content-Type": body.type});
return new Response(body, { status, headers: appendHeaders(fileHeaders, headers) });
}

export const BadRequest = toText('Bad Request', 400);
export const NotFound = toText('Not Found', 404);
export const InternalServerError = toText("Internal Server Error", 500);
161 changes: 161 additions & 0 deletions src/fetch/openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import pkg from "../../package.json" assert { type: "json" };

import { OpenApiBuilder, SchemaObject, ExampleObject, ParameterObject } from "openapi3-ts/oas31";
import { config } from "../config.js";
import { getBlock } from "../queries.js";
import { registry } from "../prometheus.js";
import { makeQuery } from "../clickhouse/makeQuery.js";
import { supportedChainsQuery } from "./chains.js";

const TAGS = {
MONITORING: "Monitoring",
HEALTH: "Health",
USAGE: "Usage",
DOCS: "Documentation",
} as const;

const chains = await supportedChainsQuery();
const block_example = (await makeQuery(await getBlock( new URLSearchParams({limit: "2"})))).data;

const timestampSchema: SchemaObject = { anyOf: [
{type: "number"},
{type: "string", format: "date"},
{type: "string", format: "date-time"}
]
};
const timestampExamples: ExampleObject = {
unix: { summary: `Unix Timestamp (seconds)` },
date: { summary: `Full-date notation`, value: '2023-10-18' },
datetime: { summary: `Date-time notation`, value: '2023-10-18T00:00:00Z'},
}

export default new OpenApiBuilder()
.addInfo({
title: pkg.name,
version: pkg.version,
description: pkg.description,
license: {name: pkg.license},
})
.addExternalDocs({ url: pkg.homepage, description: "Extra documentation" })
.addSecurityScheme("auth-key", { type: "http", scheme: "bearer" })
.addPath("/chains", {
get: {
tags: [TAGS.USAGE],
summary: 'Supported chains',
responses: {
200: {
description: "Array of chains",
content: {
"application/json": {
schema: { type: "array" },
example: chains,
}
},
},
},
},
})
.addPath("/block", {
get: {
tags: [TAGS.USAGE],
summary: "Get block",
description: "Get block by `block_number`, `block_id` or `timestamp`",
parameters: [
{
name: "chain",
in: "query",
description: "Filter by chain",
required: false,
schema: {enum: chains},
},
{
name: "block_number",
description: "Filter by Block number (ex: 18399498)",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "block_id",
in: "query",
description: "Filter by Block hash ID (ex: 00fef8cf2a2c73266f7c0b71fb5762f9a36419e51a7c05b0e82f9e3bacb859bc)",
required: false,
schema: { type: "string" },
},
{
name: 'timestamp',
in: 'query',
description: 'Filter by exact timestamp',
required: false,
schema: timestampSchema,
examples: timestampExamples,
},
{
name: "final_block",
description: "If true, only returns final blocks",
in: "query",
required: false,
schema: { type: "boolean" },
},
{
name: "sort_by",
in: "query",
description: "Sort by `block_number`",
required: false,
schema: {enum: ['ASC', 'DESC'] },
},
...["greater_or_equals_by_timestamp", "greater_by_timestamp", "less_or_equals_by_timestamp", "less_by_timestamp"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: timestampSchema,
examples: timestampExamples,
} as ParameterObject
}),
...["greater_or_equals_by_block_number", "greater_by_block_number", "less_or_equals_by_block_number", "less_by_block_number"].map(name => {
return {
name,
in: "query",
description: "Filter " + name.replace(/_/g, " "),
required: false,
schema: { type: "number" },
} as ParameterObject
}),
{
name: "limit",
in: "query",
description: "Used to specify the number of records to return.",
required: false,
schema: { type: "number", maximum: config.maxLimit, minimum: 1 },
},
],
responses: {
200: { description: "Array of blocks", content: { "application/json": { example: block_example, schema: { type: "array" } } } },
400: { description: "Bad request" },
},
},
})
.addPath("/health", {
get: {
tags: [TAGS.HEALTH],
summary: "Performs health checks and checks if the database is accessible",
responses: {200: { description: "OK", content: { "text/plain": {example: "OK"}} } },
},
})
.addPath("/metrics", {
get: {
tags: [TAGS.MONITORING],
summary: "Prometheus metrics",
responses: {200: { description: "Prometheus metrics", content: { "text/plain": { example: await registry.metrics(), schema: { type: "string" } } }}},
},
})
.addPath("/openapi", {
get: {
tags: [TAGS.DOCS],
summary: "OpenAPI specification",
responses: {200: {description: "OpenAPI JSON Specification", content: { "application/json": { schema: { type: "string" } } } }},
},
})
.getSpecAsJson();
14 changes: 14 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
declare module "*.png" {
const content: string;
export default content;
}

declare module "*.html" {
const content: string;
export default content;
}

declare module "*.sql" {
const content: string;
export default content;
}
Binary file added swagger/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions swagger/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="SwaggerUI"
/>
<title>Websocket API - SwaggerUI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css" />
<link href="/favicon.png" rel="icon" type="image/x-icon">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js" crossorigin></script>
<script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: 'https://substreams-erc20-api-production.up.railway.app/openapi',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout",
});
};
</script>
</body>
</html>

0 comments on commit c5f1367

Please sign in to comment.