Skip to content

Commit

Permalink
feat: webhooks support
Browse files Browse the repository at this point in the history
  • Loading branch information
Mokto committed Apr 30, 2024
1 parent d466721 commit 2a839d3
Show file tree
Hide file tree
Showing 8 changed files with 729 additions and 1,678 deletions.
2,158 changes: 542 additions & 1,616 deletions openapi.json

Large diffs are not rendered by default.

34 changes: 2 additions & 32 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,8 @@
import fs from 'fs';
import Database from 'libsql';
import { parseOpenAPI } from '$lib/utils/openapi';
import { prepareDatabase } from '$lib/utils/db';

console.log('Running init script...');
const dbLocation = 'openapi.db';
if (fs.existsSync(dbLocation)) {
fs.unlinkSync(dbLocation);
}
const db = new Database(dbLocation);

const data = fs.readFileSync('./openapi.json', 'utf-8');
parseOpenAPI(JSON.parse(data)).then((data) => {
db.exec(`CREATE TABLE GlobalData ( data JSON NOT NULL ) RANDOM ROWID`);
db.exec(
`INSERT INTO GlobalData (data) VALUES ('${JSON.stringify(data.global).replace(/'/g, "''")}')`
);

db.exec(
`CREATE TABLE Operations ( operation_id TEXT NOT NULL, data JSON NOT NULL ) RANDOM ROWID`
);
Object.keys(data.operations).forEach((operationId) => {
db.exec(
`INSERT INTO Operations (operation_id, data) VALUES ('${operationId}', '${JSON.stringify(data.operations[operationId]).replace(/'/g, "''")}')`
);
});
prepareDatabase(data).then(() => {
console.log('Done.');
});

// // FULL TEXT SEARCH
// // db.exec('CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize="trigram")');
// // db.exec(
// // `INSERT INTO email (sender, title, body) VALUES ('tmanoilio oi oi ioo', 'Hello', 'Hello, how are you?')`
// // );
// // db.exec(
// // `INSERT INTO email (sender, title, body) VALUES ('oie oi oi oi aaaio', 'yo', 'come on, how are you?')`
// // );
// // console.log(db.prepare('SELECT * FROM email WHERE email MATCH \'"ioo" * \' ORDER BY rank').all());
11 changes: 11 additions & 0 deletions src/lib/models/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ export interface Operation {
queryParameters: ParameterObject[];
cookieParameters: ParameterObject[];
}

export interface Webhook {
method: string;
description: string;
summary: string;
requestBody: {
type: string;
schema: SchemaObject;
examples: RequestBodyExamples;
} | null;
}
59 changes: 45 additions & 14 deletions src/lib/utils/db.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,68 @@
import fs from 'fs';
import Database from 'libsql';
import { parseOpenAPI, type GlobalData } from './openapi';
import type { OAS31Document } from '../../oas/packages/oas/src/types';
import type { Operation } from '$lib/models/operation';
import fs from 'fs';
import type { Webhook } from '../../oas/packages/oas/src/operation';

const dbLocation = 'test.db';
const dbLocation = 'openapi.db';

export const prepareDb = async (openapiDefinition: OAS31Document) => {
const data = await parseOpenAPI(openapiDefinition);
// const db = new Database(':memory:');
fs.unlinkSync(dbLocation);
export const prepareDatabase = async (jsonFile: string) => {
if (fs.existsSync(dbLocation)) {
fs.unlinkSync(dbLocation);
}
const db = new Database(dbLocation);

const data = await parseOpenAPI(JSON.parse(jsonFile));
db.exec(`CREATE TABLE GlobalData ( data JSON NOT NULL ) RANDOM ROWID`);
db.exec(
`INSERT INTO GlobalData (data) VALUES ('${JSON.stringify(data.global).replace(/'/g, "''")}')`
);

db.exec(`INSERT INTO GlobalData (data) VALUES ('${JSON.stringify(data).replace(/'/g, "''")}')`);
db.exec(
`CREATE TABLE Operations ( operation_id TEXT NOT NULL, data JSON NOT NULL ) RANDOM ROWID`
);
Object.keys(data.operations).forEach((operationId) => {
db.exec(
`INSERT INTO Operations (operation_id, data) VALUES ('${operationId}', '${JSON.stringify(data.operations[operationId]).replace(/'/g, "''")}')`
);
});

// const result = db.prepare(`SELECT data FROM GlobalData `).get();
// console.log(JSON.parse(result.data));
};
console.log(data.webhooks);

db.exec(`CREATE TABLE Webhooks ( webhook_id TEXT NOT NULL, data JSON NOT NULL ) RANDOM ROWID`);
Object.keys(data.webhooks).forEach((webhookId) => {
db.exec(
`INSERT INTO Webhooks (webhook_id, data) VALUES ('${webhookId}', '${JSON.stringify(data.webhooks[webhookId]).replace(/'/g, "''")}')`
);
});
};
export const getGlobalData = async () => {
const dbLocation = 'openapi.db';
const db = new Database(dbLocation);
const result = db.prepare(`SELECT data FROM GlobalData `).get() as { data: string };
return JSON.parse(result.data) as GlobalData;
};

export const getOperation = async (operationId: string) => {
const dbLocation = 'openapi.db';
const db = new Database(dbLocation);
const result = db
.prepare(`SELECT data FROM Operations WHERE operation_id = '${operationId}'`)
.get() as { data: string };
return JSON.parse(result.data) as Operation;
};

export const getWebhook = async (webhookId: string) => {
const db = new Database(dbLocation);
const result = db
.prepare(`SELECT data FROM Webhooks WHERE webhook_id = '${webhookId}'`)
.get() as { data: string };
return JSON.parse(result.data) as Webhook;
};

// // FULL TEXT SEARCH
// // db.exec('CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize="trigram")');
// // db.exec(
// // `INSERT INTO email (sender, title, body) VALUES ('tmanoilio oi oi ioo', 'Hello', 'Hello, how are you?')`
// // );
// // db.exec(
// // `INSERT INTO email (sender, title, body) VALUES ('oie oi oi oi aaaio', 'yo', 'come on, how are you?')`
// // );
// // console.log(db.prepare('SELECT * FROM email WHERE email MATCH \'"ioo" * \' ORDER BY rank').all());
61 changes: 51 additions & 10 deletions src/lib/utils/openapi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Operation } from '$lib/models/operation';
import type { Operation, Webhook } from '$lib/models/operation';
import Oas from '../../oas/packages/oas/src';

import type { MediaTypeObject, SchemaObject, ServerObject } from 'openapi3-ts/oas31';
Expand All @@ -20,15 +20,15 @@ interface Topic {
example: string;
}

const addToMenu = (menu: Menu, title: string, label: string, tag: string, link: string) => {
const addToMenu = (menu: Menu, title: string, label: string, link: string) => {
// Find the group first and add an item to it. If the group doesn't exist, create it.
const group = menu.find((group) => group.title === title);
if (group) {
group.items.push({ label, tag, link });
group.items.push({ label, link });
} else {
menu.push({
title,
items: [{ label, tag, link }]
items: [{ label, link }]
});
}
};
Expand All @@ -54,12 +54,12 @@ export const parseOpenAPI = async (openapi: OAS31Document) => {
const servers: ServerObject[] = oas.api.servers || [];

if (topics?.length) {
addToMenu(menu, 'Topics', 'Introduction', 'introduction', '/');
addToMenu(menu, 'Topics', 'Introduction', '/');
topics.forEach((topic) => {
addToMenu(menu, 'Topics', topic.title, topic.id, `/topics/${topic.id}`);
addToMenu(menu, 'Topics', topic.title, `/topics/${topic.id}`);
});
} else {
addToMenu(menu, '', 'Introduction', 'introduction', '/introduction');
addToMenu(menu, '', 'Introduction', '/introduction');
}

const operations: { [operationId: string]: Operation } = {};
Expand Down Expand Up @@ -122,13 +122,53 @@ export const parseOpenAPI = async (openapi: OAS31Document) => {

const tags = op.getTags();
if (!tags?.length) {
addToMenu(menu, 'Default', op.getSummary(), method, `/operations/${op.getOperationId()}`);
addToMenu(menu, 'Default', op.getSummary(), `/operations/${op.getOperationId()}`);
}
tags.forEach((tag) => {
addToMenu(menu, tag.name, op.getSummary(), method, `/operations/${op.getOperationId()}`);
addToMenu(menu, tag.name, op.getSummary(), `/operations/${op.getOperationId()}`);
});
}
}

const webhooks: { [webhookId: string]: Webhook } = {};

for (const operation of Object.values(oas.getWebhooks())) {
for (const method in operation) {
const op = operation[method as OpenAPIV3_1.HttpMethods];
if (!op.hasOperationId()) {
continue;
}

let requestBody: Webhook['requestBody'] = null;
const requestBodyData = op.getRequestBody() as [string, MediaTypeObject];
if (requestBodyData?.length) {
requestBody = {
type: requestBodyData[0],
schema: requestBodyData[1].schema as SchemaObject,
examples: op.getRequestBodyExamples()
};
}

webhooks[op.getOperationId()] = {
description: op.getDescription(),
summary: op.getSummary(),
// path: op.path,
method: method,
requestBody
};

addToMenu(menu, 'Webhooks', `${op.getOperationId()}`, `/webhooks/${op.getOperationId()}`);
}
}

// const webhookKeys = Object.keys(webhooks);
// webhookKeys.forEach((key) => {
// const methodKeys = Object.keys(webhooks[key]);
// methodKeys.forEach((method) => {
// addToMenu(menu, 'Webhooks', `${key}`, `/webhooks/${method}_${key}`);
// });
// });

return {
global: {
menu,
Expand All @@ -138,6 +178,7 @@ export const parseOpenAPI = async (openapi: OAS31Document) => {
version,
servers
},
operations
operations,
webhooks
};
};
7 changes: 1 addition & 6 deletions src/routes/operations/[...page]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { getOperation } from '$lib/utils/db.js';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const operationId = params.page.split('/').pop();
if (!operationId) {
return {
isHomepage: true
};
}
const operationId = params.page;
const operation = await getOperation(operationId);
if (!operation) {
return {};
Expand Down
15 changes: 15 additions & 0 deletions src/routes/webhooks/[...page]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getWebhook } from '$lib/utils/db.js';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const webhookId = params.page;
const webhook = await getWebhook(webhookId);
if (!webhook) {
return {};
}

return {
webhook,
webhookId
};
}
62 changes: 62 additions & 0 deletions src/routes/webhooks/[...page]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import type { PageData } from './$types';
import MethodTag from '$lib/components/tag/method-tag.svelte';
import DocumentedProperty from '$lib/components/documented-property/documented-property.svelte';
import Markdown from '$lib/components/markdown/markdown.svelte';
import CodeBlock from '$lib/components/code-block/code-block.svelte';
import UsageExample from '$lib/components/usage-example/usage-example.svelte';
import Tabs from '$lib/components/tab/tabs.svelte';
import Tab from '$lib/components/tab/tab.svelte';
import TabHead from '$lib/components/tab/tab-head.svelte';
import TabHeadItem from '$lib/components/tab/tab-head-item.svelte';
/** @type {import('./$types').PageData} */
export let data: PageData;
</script>

{#key data.webhookId}
{#if data.webhook}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 px-12 py-12 max-w-screen-xl mx-auto">
<div>
<h1 class="text-2xl font-bold mb-2">{data.webhook.summary}</h1>
<div class="mb-4">
<MethodTag method={data.webhook.method} />
</div>
<Markdown markdown={data.webhook.description} />
</div>
</div>
<hr />

{#if data.webhook.requestBody}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 px-12 py-12 max-w-screen-xl mx-auto">
<div>
<div class="flex items-center">
<h2 class="!my-2 text-lg font-bold mt-4 mb-2">Body</h2>
<div class="flex-1"></div>
<div class="text-sm">{data.webhook.requestBody.type}</div>
</div>
<hr class="mb-4" />

<DocumentedProperty parameter={data.webhook.requestBody?.schema} />
</div>
{#if data.webhook?.requestBody?.examples}
<div>
{#each data.webhook?.requestBody?.examples as example}
<CodeBlock
language="json"
headerText={`Request example: ${example.mediaType}`}
code={JSON.stringify(example.examples[0].value, null, 2)}
/>
{/each}
</div>
{/if}
</div>
<hr />
{/if}
{:else}
No webhook found.

<br />
<a href="/">Go back to home page</a>
{/if}
{/key}

0 comments on commit 2a839d3

Please sign in to comment.