mirror of
https://github.com/Drop-OSS/drop-api-autogen.git
synced 2026-01-30 20:55:17 +01:00
feat: more tweaks and features
This commit is contained in:
@@ -1,26 +1,49 @@
|
||||
<template>
|
||||
<ul class="space-y-3">
|
||||
<li v-for="[path, methods] in collapsedAPINav.entries()">
|
||||
<span class="font-display text-zinc-100 font-semibold">{{ path }}</span>
|
||||
<ul class="mt-1 space-y-1">
|
||||
<li v-for="method in methods">
|
||||
<NuxtLink
|
||||
class="transition hover:bg-zinc-900 px-1 py-0.5 rounded ml-1 text-zinc-400 grid grid-cols-4 gap-x-2"
|
||||
:href="method.path">
|
||||
|
||||
<span
|
||||
:class="[apiMethodColours[method.method], 'text-center text-xs font-bold ring-1 rounded-full px-1 py-0.5']">{{
|
||||
method.method }}</span>
|
||||
<div class="col-span-3 text-sm relative whitespace-nowrap overflow-hidden"><span
|
||||
class="absolute right-0">{{ method.methodlessName }}</span></div>
|
||||
<div>
|
||||
<div v-if="guidesNavigation">
|
||||
<h1 class="font-display font-bold text-zinc-100">{{ guidesNavigation.title }}</h1>
|
||||
<ul class="ml-3 mt-1 space-y-2">
|
||||
<li v-for="guide in guidesNavigation.children">
|
||||
<NuxtLink :href="guide.path" class="transition text-sm text-zinc-300 hover:text-zinc-100">
|
||||
<span class="text-blue-400">+ {{ " " }}</span>{{ guide.title }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<div class="grid grid-cols-1">
|
||||
<input type="email" name="email" id="email"
|
||||
class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-950 py-1.5 pr-3 pl-10 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-500 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6"
|
||||
placeholder="/api/v1/..." v-model="query" />
|
||||
<MagnifyingGlassIcon
|
||||
class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-gray-400 sm:size-4"
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="mt-4 space-y-3">
|
||||
<li v-for="[path, methods] in filteredCollapsedNav.entries()">
|
||||
<span class="font-display text-zinc-100 font-semibold">{{ path }}</span>
|
||||
<ul class="mt-1 space-y-1">
|
||||
<li v-for="method in methods">
|
||||
<NuxtLink
|
||||
class="transition hover:bg-zinc-900 px-1 py-0.5 rounded ml-1 text-zinc-400 grid grid-cols-4 gap-x-2"
|
||||
:href="method.path">
|
||||
|
||||
<span
|
||||
:class="[apiMethodColours[method.method], 'text-center text-xs font-bold ring-1 rounded-full px-1 py-0.5']">{{
|
||||
method.method }}</span>
|
||||
<div class="col-span-3 text-sm relative whitespace-nowrap overflow-hidden"><span
|
||||
class="absolute right-0">{{ method.methodlessName }}</span></div>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MagnifyingGlassIcon } from '@heroicons/vue/16/solid'
|
||||
import type { ContentNavigationItem } from '@nuxt/content';
|
||||
|
||||
const rawAPINav = await queryCollectionNavigation("docs");
|
||||
@@ -66,4 +89,20 @@ collapsedAPINav.value = new Map([...collapsedAPINav.value].sort((a, b) => a[0].l
|
||||
return v;
|
||||
}))
|
||||
|
||||
const query = ref("");
|
||||
|
||||
const filteredCollapsedNav = computed(() => {
|
||||
if(!query.value) return collapsedAPINav.value;
|
||||
|
||||
const raw = [...collapsedAPINav.value];
|
||||
|
||||
const filtered = raw.map((e) => [e[0], e[1].filter((v) => v.title.toLowerCase().includes(query.value.toLowerCase()))] as const);
|
||||
const stripped = filtered.filter((e) => e[1].length > 0);
|
||||
|
||||
return new Map(stripped);
|
||||
|
||||
});
|
||||
|
||||
const guidesNavigation = (await queryCollectionNavigation("guides")).at(0);
|
||||
|
||||
</script>
|
||||
19
app/components/content/Deprecated.vue
Normal file
19
app/components/content/Deprecated.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="not-prose rounded-md bg-yellow-600/10 p-4 my-4">
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<ExclamationTriangleIcon class="size-5 text-yellow-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="-mt-1 text-sm font-medium text-yellow-400">Deprecated</h3>
|
||||
<div class="mt-2 text-sm text-yellow-500">
|
||||
<p>This route has been deprecated, and is not recommended for new projects.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ExclamationTriangleIcon } from '@heroicons/vue/20/solid'
|
||||
</script>
|
||||
3
app/components/content/ProseH2.vue
Normal file
3
app/components/content/ProseH2.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1 class="not-prose text-zinc-100 text-3xl font-bold font-display"><slot /></h1>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-zinc-950 h-screen w-full">
|
||||
<div class="h-screen w-full">
|
||||
<div class="h-full lg:ml-72 xl:ml-80">
|
||||
<div class="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex">
|
||||
<div
|
||||
@@ -10,7 +10,7 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<Header />
|
||||
<Navigation class="hidden lg:mt-10 lg:block" />
|
||||
<Navigation class="hidden lg:mt-6 lg:block" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-y-auto flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -3,8 +3,6 @@ const route = useRoute();
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
|
||||
|
||||
page.value?.body.toc
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.title,
|
||||
description: page.value?.description
|
||||
@@ -13,8 +11,8 @@ useSeoMeta({
|
||||
|
||||
<template>
|
||||
<div v-if="page" class="flex flex-row gap-8">
|
||||
<ContentRenderer class="grow prose prose-invert prose-blue max-w-none" :value="page" />
|
||||
<div class="sticky top-0 w-96 h-min" v-if="page.body.toc">
|
||||
<ContentRenderer class="grow prose prose-invert prose-blue max-w-none" :value="page" :prose="true" />
|
||||
<div class="hidden lg:block sticky top-0 w-96 h-min" v-if="page.body.toc">
|
||||
<h1 class="text-zinc-400">On this page</h1>
|
||||
<ul class="mt-1 ">
|
||||
<li v-for="link in page.body.toc.links">
|
||||
@@ -26,5 +24,4 @@ useSeoMeta({
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>Page not found</div>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,9 @@ useSeoMeta({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentRenderer v-if="page" :value="page" />
|
||||
<div v-if="page" class="prose prose-invert prose-blue">
|
||||
<ContentRenderer :value="page" />
|
||||
|
||||
</div>
|
||||
<div v-else>Page not found</div>
|
||||
</template>
|
||||
|
||||
2
drop
2
drop
Submodule drop updated: 29fdfcbdd4...7c234067a5
@@ -1 +0,0 @@
|
||||
# How to create an API token
|
||||
11
guides/generating-token.md
Normal file
11
guides/generating-token.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Generating an API token
|
||||
|
||||
API tokens can be manually created by a user, or you can provide a convenient URL that auto-selects all the ACLs you want. Of course, users can edit these ACLs, but it is still a nice-to-have if you are developing a third-party application.
|
||||
|
||||
## How it works
|
||||
|
||||
The API token generation mimics OAuth2/SSO flow. At a high level, this is how it works:
|
||||
|
||||
1. Third party application (you) generates an "authorization" URL. This specifies the callback URL, required ACLs, and application metadata.
|
||||
2. Third party application opens authorization URL in browser. The user, if signed in, can approve, modify, or deny the request.
|
||||
3. If approved, Drop generates the API token, and redirects the URL to the callback URL, with the token in tow.
|
||||
@@ -12,10 +12,20 @@ export default defineNuxtConfig({
|
||||
css: ["~/assets/css/core.scss", "~/assets/css/tailwind.css"],
|
||||
|
||||
hooks: {
|
||||
"build:before": async () => {
|
||||
"modules:before": async () => {
|
||||
// Dynamic import so I can ignore errors. (I'm lazy)
|
||||
const build = (await import("./tools/index")).default;
|
||||
await build();
|
||||
},
|
||||
},
|
||||
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
highlight: {
|
||||
theme: "aurora-x",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@nuxt/content": "^3.6.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
|
||||
@@ -8,6 +8,27 @@ function encouragePrettierLinebreaks(rawType: string) {
|
||||
: rawType;
|
||||
}
|
||||
|
||||
async function renderType(metadata: RouteMetadata, type: ts.Type) {
|
||||
const expandedType = metadata.checker.typeToTypeNode(
|
||||
type,
|
||||
metadata.sourceFile,
|
||||
ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.InTypeAlias
|
||||
);
|
||||
|
||||
const rawType = metadata.checker.typeToString(
|
||||
type,
|
||||
undefined,
|
||||
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias
|
||||
);
|
||||
|
||||
const newLinedType = encouragePrettierLinebreaks(rawType);
|
||||
const pretty = await prettier.format(`type Body = ${newLinedType}`, {
|
||||
parser: "typescript",
|
||||
});
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
export default async function generateDocsPage(
|
||||
route: Route,
|
||||
metadata: RouteMetadata
|
||||
@@ -26,17 +47,7 @@ Request type: \`application/json\`. Body type (TypeScript definition):
|
||||
`.trimStart();
|
||||
bodyText += "```typescript\n";
|
||||
|
||||
const rawType = metadata.checker.typeToString(
|
||||
metadata.body,
|
||||
undefined,
|
||||
ts.TypeFormatFlags.NoTruncation
|
||||
);
|
||||
const newLinedType = encouragePrettierLinebreaks(rawType);
|
||||
const pretty = await prettier.format(`type Body = ${newLinedType}`, {
|
||||
parser: "typescript",
|
||||
});
|
||||
|
||||
bodyText += pretty;
|
||||
bodyText += await renderType(metadata, metadata.body);
|
||||
bodyText += "\n```";
|
||||
} else if (metadata.bodyComment) {
|
||||
bodyText = `
|
||||
@@ -59,9 +70,10 @@ ${metadata.bodyComment ?? ""}`;
|
||||
if (metadata.query) {
|
||||
const queryProperties = metadata.query.getProperties();
|
||||
if (queryProperties.length > 0) {
|
||||
queryTable = "## Query Parameters\n\n| Name | Type |\n| ---- | ---- |\n";
|
||||
queryTable =
|
||||
"## Query Parameters\n\n| Name | Parser |\n| ---- | ---- |\n";
|
||||
for (const property of queryProperties) {
|
||||
queryTable += `| ${property.escapedName} | string |\n`;
|
||||
queryTable += `| ${property.escapedName} | ${property.declarations?.at(0)?.getLastToken()?.getText()?.slice(1, -1) ?? "string"} |\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,9 +89,9 @@ ${metadata.acls.map((e) => ` - \`${e}\``).join("\n")}
|
||||
`.trimStart();
|
||||
}
|
||||
// If ACL is checked, but no specific ones are required
|
||||
// Means authenticated, but no specific ACL is required
|
||||
// Means authenticated, but only sessions
|
||||
else if (metadata.acls) {
|
||||
routeAuthentication = `You can access this route with any \`${metadata.aclMode}\` token.`;
|
||||
routeAuthentication = `Only \`${metadata.aclMode}\` **sessions** can access this route, not API tokens.`;
|
||||
}
|
||||
|
||||
// ===== Client Warning =====
|
||||
@@ -104,17 +116,7 @@ Response definition:
|
||||
`.trimStart();
|
||||
responseText += "```typescript\n";
|
||||
|
||||
const rawType = metadata.checker.typeToString(
|
||||
metadata.response,
|
||||
undefined,
|
||||
ts.TypeFormatFlags.NoTruncation
|
||||
);
|
||||
const newLinedType = encouragePrettierLinebreaks(rawType);
|
||||
const pretty = await prettier.format(`type Response = ${newLinedType}`, {
|
||||
parser: "typescript",
|
||||
});
|
||||
|
||||
responseText += pretty;
|
||||
responseText += await renderType(metadata, metadata.response);
|
||||
responseText += "\n```";
|
||||
} else if (metadata.responseComment) {
|
||||
responseText = `
|
||||
@@ -122,9 +124,16 @@ Response definition:
|
||||
${metadata.responseComment}`.trim();
|
||||
}
|
||||
|
||||
const deprecatedElement = `::deprecated\n::`;
|
||||
|
||||
const rawText = `
|
||||
---
|
||||
metaUrl: ${route.urlPath}
|
||||
---
|
||||
# ${route.method} \`${route.urlPath}\`
|
||||
|
||||
${metadata.deprecated ? deprecatedElement : ""}
|
||||
|
||||
## About this route
|
||||
${metadata.routeDescription ?? "*No description provided.*"}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ async function transformRoutes(base: string, routes: string[]) {
|
||||
|
||||
export interface RouteMetadata {
|
||||
checker: ts.TypeChecker;
|
||||
sourceFile: ts.SourceFile;
|
||||
deprecated?: boolean;
|
||||
|
||||
body?: ts.Type;
|
||||
bodyComment?: string;
|
||||
@@ -122,16 +124,20 @@ async function generateDocs(
|
||||
if (!ts.isCallExpression(eventHandler)) continue;
|
||||
|
||||
// Check if this is defineClientHandler, and mark as a client route if so
|
||||
const eventHandlerName = eventHandler.expression.getText();
|
||||
const isClient = eventHandlerName.toLowerCase().includes("client");
|
||||
const eventHandlerName = eventHandler.expression.getText().toLowerCase();
|
||||
const isClient = eventHandlerName.includes("client");
|
||||
|
||||
const metadata: RouteMetadata = { checker, clientRoute: isClient };
|
||||
const metadata: RouteMetadata = {
|
||||
checker,
|
||||
clientRoute: isClient,
|
||||
sourceFile: routeTs,
|
||||
};
|
||||
|
||||
// Extract the body type from the generic args
|
||||
// Complicated and messy
|
||||
|
||||
// Only run on client/normal handlers
|
||||
if (!eventHandlerName.toLowerCase().includes("websocket")) {
|
||||
if (!eventHandlerName.includes("websocket")) {
|
||||
const resolvedSignature = checker.getResolvedSignature(eventHandler);
|
||||
if (!resolvedSignature)
|
||||
throw "No resolved signature? Somehow an invalid node?";
|
||||
@@ -144,12 +150,14 @@ async function generateDocs(
|
||||
);
|
||||
if (handlerType.isUnion()) handlerType = handlerType.types.at(0)!;
|
||||
|
||||
const handlerSignature = handlerType.getCallSignatures().at(0)!;
|
||||
|
||||
const typeArguments = (handlerType as any)[
|
||||
"resolvedTypeArguments"
|
||||
] as ts.Type[];
|
||||
|
||||
if (typeArguments) {
|
||||
const requestConfigurationType = typeArguments.at(0)!;
|
||||
const requestConfigurationType = typeArguments.at(isClient ? 1 : 0)!;
|
||||
|
||||
const properties = requestConfigurationType.getProperties();
|
||||
for (const property of properties) {
|
||||
@@ -162,9 +170,11 @@ async function generateDocs(
|
||||
(metadata as any)[property.escapedName!] = type;
|
||||
}
|
||||
|
||||
const responseType = typeArguments.at(1)!;
|
||||
if (!(responseType.flags & ts.TypeFlags.Any))
|
||||
metadata.response = checker.getAwaitedType(responseType);
|
||||
const responseType = checker.getReturnTypeOfSignature(handlerSignature);
|
||||
const awaited = checker.getAwaitedType(responseType);
|
||||
if (awaited && !(awaited.flags & ts.TypeFlags.Any)) {
|
||||
metadata.response = awaited;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it's websocket, change the "method"
|
||||
@@ -239,6 +249,10 @@ async function generateDocs(
|
||||
metadata.responseComment = tag.comment?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isJSDocDeprecatedTag(tag)) {
|
||||
metadata.deprecated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -564,6 +564,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-3.1.1.tgz#af3aea7f1e52ec916d8b5c9dcc0f09d4c060a3fc"
|
||||
integrity sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==
|
||||
|
||||
"@headlessui/vue@^1.7.23":
|
||||
version "1.7.23"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/vue/-/vue-1.7.23.tgz#7fe19dbeca35de9e6270c82c78c4864e6a6f7391"
|
||||
integrity sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==
|
||||
dependencies:
|
||||
"@tanstack/vue-virtual" "^3.0.0-beta.60"
|
||||
|
||||
"@heroicons/vue@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/vue/-/vue-2.2.0.tgz#d81f14eed448eec9859849ed63facd3f29bca2b3"
|
||||
integrity sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==
|
||||
|
||||
"@ioredis/commands@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.3.0.tgz#4dc3ae9bfa7146b63baf27672a61db0ea86e35e5"
|
||||
@@ -1891,6 +1903,18 @@
|
||||
"@tailwindcss/oxide" "4.1.11"
|
||||
tailwindcss "4.1.11"
|
||||
|
||||
"@tanstack/virtual-core@3.13.12":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
|
||||
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
|
||||
|
||||
"@tanstack/vue-virtual@^3.0.0-beta.60":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz#a66daac9e6822ce4bcba76a3954937440697c264"
|
||||
integrity sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.13.12"
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
|
||||
Reference in New Issue
Block a user