Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: serve AsyncAPI JSON Schema definitions #680

Merged
merged 19 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ npm run build

Generated files of the website go to the `.next` folder.

## JSON Schema definitions

All AsyncAPI JSON Schema definition files are being served within the `/definitions/<file>` path. The content is being served from GH, in particular from https://github.com/asyncapi/spec-json-schemas/tree/master/schemas.
This is possible thanks to the following:

1. A [Netlify Rewrite rule](https://docs.netlify.com/routing/redirects/rewrites-proxies/) located in the [netlify.toml](netlify.toml) file, which acts as proxy for all requests to the `/definitions/<file>` path, serving the content from GH without having an HTTP redirect.
2. A [Netlify Edge Function](https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/) that modifies the `Content-Type` header of the rewrite response to become `application/schema+json`. This lets tooling, such as [Hyperjump](https://json-schema.hyperjump.io), to fetch the schemas directly from their URL.

## Project structure

This repository has the following structure:
Expand All @@ -85,6 +93,8 @@ This repository has the following structure:
├── public # Data for site metadata and static blog such as images
├── scripts # Scripts used in the build and dev processes
├── next.config.js # Next.js configuration file
├── netlify # Code that runs on Netlify
│ ├── edge-functions # Netlify Edge-Functions code
├── postcss.config.js # PostCSS configuration file
└── tailwind.config.js # TailwindCSS configuration file
```
Expand Down
32 changes: 31 additions & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,34 @@
command = "npm run build && npm run export"
functions = "netlify/functions"
publish = "out"


# Used by JSON Schema definitions fetched directly from AsyncAPI website
[[redirects]]
from = "/definitions"
to = "https://github.com/asyncapi/spec-json-schemas/tree/master/schemas"
status = 200

[[redirects]]
from = "/definitions/*"
to = "https://raw.githubusercontent.com/asyncapi/spec-json-schemas/master/schemas/:splat"
status = 200

[[edge_functions]]
function = "serve-definitions"
path = "/definitions/*"

# Used by JSON Schema definitions fetched from schemastore.org
[[redirects]]
from = "/schema-store"
to = "https://github.com/asyncapi/spec-json-schemas/tree/master/schemas"
status = 200

[[redirects]]
from = "/schema-store/*"
to = "https://raw.githubusercontent.com/asyncapi/spec-json-schemas/master/schemas/:splat"
status = 200

[[edge_functions]]
function = "serve-definitions"
path = "/schema-store/*"

97 changes: 97 additions & 0 deletions netlify/edge-functions/serve-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Context } from "netlify:edge";

const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN");
smoya marked this conversation as resolved.
Show resolved Hide resolved
const NR_ACCOUNT = Deno.env.get("NR_ACCOUNT");
const NR_API_KEY = Deno.env.get("NR_API_KEY");

export default async (request: Request, context: Context) => {
// Deleting Origin header, which is involved in the cache policy, so requests can hit GH cache.
// Reason: raw.githubusercontent.com responses include vary: Authorization,Accept-Encoding,Origin
request.headers.delete("origin");

// Setting GH Token to increase GH rate limit to 5,000 req/h.
request.headers.set("Authorization", "token " + GITHUB_TOKEN);

// Fetching the definition file
const response = await context.next();

const isRequestingAFile = request.url.charAt(request.url.length - 1) !== "/";
if (isRequestingAFile) {
if (response.ok) {
// Setting proper Content-Type header for JSON Schema files.
// This lets tooling fetch the schemas directly from their URL.
response.headers.set("Content-Type", "application/schema+json");

// Sending metrics to NR.
const event = {
"eventType": "AsyncAPIJSONSchemaDefinitionDownload",
};

await sendEventToNR(request, context, event);
} else {
// Notifying NR of the error.
const event = {
"eventType": "AsyncAPIJSONSchemaDefinitionDownloadError",
"responseStatus": response.status,
"responseStatusText": response.statusText,
};

await sendEventToNR(request, context, event);
}
}

return response;
};

interface TimeoutRequestInit extends RequestInit {
timeout: number;
}

async function doFetch(resource: string, options: TimeoutRequestInit): Promise<Response> {
const { timeout = 5000 } = options;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {...options, signal: controller.signal});
clearTimeout(timeoutId);
return response;
}

interface NREvent {
eventType: string;
url?: string;
source?: string;
file?: string;
}

async function sendEventToNR(request: Request, context: Context, event: NREvent) {
const splitPath = new URL(request.url).pathname.split("/");
if (event.source === "" || event.source === undefined) {
event.source = splitPath[1];
event.file = splitPath[2];
}

event.url = request.url;

try {
const rawResponse = await doFetch(`https://insights-collector.eu01.nr-data.net/v1/accounts/${NR_ACCOUNT}/events`, {
timeout: 2000, // Success in 2 seconds, cancel if not. User's request is more important than collecting metrics.
method: 'POST',
headers: {
'Api-Key': NR_API_KEY || "",
'Content-Type': 'application/json'
},
body: JSON.stringify(event)
});

if (rawResponse.status !== 200) {
context.log(`Unexpected response status code when sending metrics: ${rawResponse.status} ${rawResponse.statusText}`);
}
} catch (e) {
if (e instanceof DOMException) {
context.log(`Timeout during sending metrics: ${e}`);
} else {
context.log(`Unexpected error sending metrics: ${e}`);
}
}
}