mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Feature | API Playground (#565)
* CHANGED: - Added InteractiveButton component - Updated APIInfo design and added ability to copy the endpoint url - Installed shiki to fumadocs-ui - Added api-context Signed-off-by: kyrylolvov <[email protected]> * CHANGED: - Updated APIInfo to render differently depending on whether children are passed - Added SendButton component Signed-off-by: kyrylolvov <[email protected]> * CHANGED: - Added statusMap and getStatusInfo function which returns description and icon for response Signed-off-by: kyrylolvov <[email protected]> * CHANGED: - Added a new CodeBlock component, which is used to render API response Signed-off-by: kyrylolvov <[email protected]> * CHANGED: - Installed required radix-ui dependencies for input, label, form shadcn components - Added APIPlayground and ArrayInput components to API Signed-off-by: kyrylolvov <[email protected]> * CHANGED: - Updated defaultBaseUrl in RootProvider - Fixed errors - Updated docs Signed-off-by: kyrylolvov <[email protected]> * Update pnpm-lock * Rewrite API Playground * Clean code * OpenAPI: Support passing base url to Root component * Update lock file * Update test snapshots * Fix lint issues * Update API usages * OpenAPI & UI: Generate Playground from MDX files * Fix problems * Improve design * Handle self-referencing types * Clean code * Add switcher UI * Add unkey for testing * OpenAPI: Combine `allOf` into one object schema * Update unkey.json * OpenAPI & UI: Support optional boolean values * OpenAPI & UI: Support `additionalProperties` field * Update test snapshots * OpenAPI & UI: Clean code & support removing additional properties * OpenAPI: Improve title generation * UI: Improve implmentation * OpenAPI & UI: Improve Implementation * Remove unkey.json from docs * OpenAPI & UI: Move UI part of OpenAPI to `fumadocs-openapi` * OpenAPI & UI: Improve UI implementation * OpenAPI: Fix dynamic references issues * OpenAPI: Handle unregistering additional properties * Docs: Rewrite docs --------- Signed-off-by: kyrylolvov <[email protected]> Co-authored-by: fuma-nama <[email protected]> Co-authored-by: Fuma Nama <[email protected]>
This commit is contained in:
co-authored by
fuma-nama
Fuma Nama
parent
c79f42a332
commit
ad143e19f5
@@ -0,0 +1,30 @@
|
||||
---
|
||||
'fumadocs-openapi': major
|
||||
---
|
||||
|
||||
Move UI implementation from `fumadocs-ui` to `fumadocs-openapi`.
|
||||
|
||||
**why:** Allow a better organization of packages.
|
||||
|
||||
**migrate:**
|
||||
|
||||
This package is now Tailwind CSS only, you need to use it in conjunction with the official Tailwind CSS plugin.
|
||||
|
||||
Add the package to `content` under your Tailwind CSS configuration.
|
||||
|
||||
```js
|
||||
import { createPreset, presets } from 'fumadocs-ui/tailwind-plugin';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./node_modules/fumadocs-ui/dist/**/*.js',
|
||||
'./node_modules/fumadocs-openapi/dist/**/*.js',
|
||||
],
|
||||
presets: [
|
||||
createPreset(),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Re-generate MDX files if needed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Implement OpenAPI playground
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Support passing base url to Root component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Combine `allOf` into one object schema
|
||||
@@ -53,6 +53,16 @@ adding it to an existing codebase or implementing advanced routing.
|
||||
| Built-in Components | Yes | Yes |
|
||||
| RTL Layout | Yes | Yes |
|
||||
|
||||
### Additional Features
|
||||
|
||||
Features supported via 3rd party libraries like [TypeDoc](https://typedoc.org) will not be listed here.
|
||||
|
||||
| Feature | Fumadocs | Nextra |
|
||||
| -------------------------- | -------- | ------ |
|
||||
| OpenAPI Integration | Yes | No |
|
||||
| TypeScript Docs Generation | Yes | No |
|
||||
| TypeScript Twoslash | Yes | No |
|
||||
|
||||
## Mintlify
|
||||
|
||||
Mintlify is a documentation service, as compared to Fumadocs, it offers a free tier while isn't completely free and open source.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: API Example
|
||||
description: Container for examples of request and response.
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Import it in your MDX documents.
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { API, APIExample } from 'fumadocs-ui/components/api';
|
||||
|
||||
<API>
|
||||
<APIExample>...</APIExample>
|
||||
</API>
|
||||
```
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
title: API Info
|
||||
description: Components for basic information of API endpoint.
|
||||
---
|
||||
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method={'POST'} route={'/tickets'}></APIInfo>
|
||||
|
||||
## Usage
|
||||
|
||||
Import it in your MDX documents.
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { API, APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<API>
|
||||
<APIInfo method="POST" route="/tickets">
|
||||
...
|
||||
</APIInfo>
|
||||
</API>
|
||||
```
|
||||
|
||||
## Variants
|
||||
|
||||
Depending on the `method` prop, the APIInfo component will render different variants of the component.
|
||||
|
||||
### GET
|
||||
|
||||
<APIInfo method="GET" route="/tickets"></APIInfo>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method="GET" route="/tickets">
|
||||
...
|
||||
</APIInfo>
|
||||
```
|
||||
|
||||
### POST
|
||||
|
||||
<APIInfo method="POST" route="/tickets"></APIInfo>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method="POST" route="/tickets">
|
||||
...
|
||||
</APIInfo>
|
||||
```
|
||||
|
||||
### PUT
|
||||
|
||||
<APIInfo method="PUT" route="/tickets"></APIInfo>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method="PUT" route="/tickets">
|
||||
...
|
||||
</APIInfo>
|
||||
```
|
||||
|
||||
### PATCH
|
||||
|
||||
<APIInfo method={'PATCH'} route={'/tickets'}></APIInfo>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method={'PATCH'} route={'/tickets'}>
|
||||
...
|
||||
</APIInfo>
|
||||
```
|
||||
|
||||
### DELETE
|
||||
|
||||
<APIInfo method="DELETE" route="/tickets"></APIInfo>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { APIInfo } from 'fumadocs-ui/components/api';
|
||||
|
||||
<APIInfo method="DELETE" route="/tickets">
|
||||
...
|
||||
</APIInfo>
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```json doc-gen:typescript
|
||||
{
|
||||
"file": "./content/docs/ui/props.ts",
|
||||
"name": "APIInfoProps"
|
||||
}
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: OpenAPI
|
||||
description: OpenAPI Components
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
Fumadocs UI provides built-in components for the official [OpenAPI Integration](/docs/ui/openapi), including:
|
||||
|
||||
| component | description |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| Root | The wrapper of entire document |
|
||||
| API | Container for each operation |
|
||||
| APIInfo | Container for basic operation information |
|
||||
| APIExample | Container for API usage examples |
|
||||
| Responses | Container (Tabs) for response examples |
|
||||
| Response | Container (Tab) for each response example |
|
||||
| ResponseTypes | Container for different schema types of a response example |
|
||||
| ExampleResponse | JSON schema of a response example |
|
||||
| TypeScriptResponse | TypeScript schema of a response example |
|
||||
| Property | The card for describing a property |
|
||||
| ObjectCollapsible | An accordion to describe additional info of object |
|
||||
| Requests | Container for request examples |
|
||||
| Request | Container for each request example |
|
||||
|
||||
```ts
|
||||
import { ... } from 'fumadocs-ui/components/api';
|
||||
```
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Property
|
||||
description: The component which provides detailed information about an API endpoint's properties.
|
||||
---
|
||||
|
||||
import { Property } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Property name={'Authorization'} type={'Basic <token>'} required={true} />
|
||||
|
||||
## Usage
|
||||
|
||||
Import it in your MDX documents.
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { Property } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Property name={'Authorization'} type={'Basic <token>'} required={true} />
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
The `Property` component supports nesting, allowing you to display objects and arrays.
|
||||
|
||||
```mdx title="users.mdx"
|
||||
import { Property } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Property name={'credentials'} type={'object'} required={true}>
|
||||
<Property name={'username'} type={'string'} />
|
||||
<Property name={'password'} type={'string'} />
|
||||
</Property>
|
||||
```
|
||||
|
||||
<Property name={'credentials'} type={'object'} required={true}>
|
||||
<Property name={'username'} type={'string'} />
|
||||
<Property name={'password'} type={'string'} />
|
||||
</Property>
|
||||
|
||||
The `Property` component allows children to be passed, so that you can display additional information about the property.
|
||||
|
||||
<Property name={'Authorization'} type={'Basic <token>'} required={true}>
|
||||
In: `header`
|
||||
</Property>
|
||||
|
||||
```mdx title="tickets.mdx"
|
||||
import { Property } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Property name={'Authorization'} type={'Basic <token>'} required={true}>
|
||||
In: `header`
|
||||
</Property>
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
```json doc-gen:typescript
|
||||
{
|
||||
"file": "./content/docs/ui/props.ts",
|
||||
"name": "PropertyProps"
|
||||
}
|
||||
```
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
title: Requests
|
||||
description: Components for request examples.
|
||||
---
|
||||
|
||||
import { Requests, Request } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Requests items={["cURL","JavaScript"]}>
|
||||
|
||||
<Request value="cURL">
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.fake-museum-example.com/special-events"
|
||||
```
|
||||
|
||||
</Request>
|
||||
|
||||
<Request value="JavaScript">
|
||||
|
||||
```js
|
||||
fetch('https://api.fake-museum-example.com/special-events', {
|
||||
method: 'GET',
|
||||
});
|
||||
```
|
||||
|
||||
</Request>
|
||||
|
||||
</Requests>
|
||||
|
||||
## Usage
|
||||
|
||||
Import it in your MDX documents.
|
||||
|
||||
````mdx title="tickets.mdx"
|
||||
import { Requests, Request } from 'fumadocs-ui/components/api';
|
||||
|
||||
<Requests items={["cURL","JavaScript"]}>
|
||||
<Request value="cURL">
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.fake-museum-example.com/special-events"}
|
||||
```
|
||||
|
||||
</Request>
|
||||
<Request value="JavaScript">
|
||||
|
||||
```js
|
||||
fetch('https://api.fake-museum-example.com/special-events', {
|
||||
method: 'GET',
|
||||
});
|
||||
```
|
||||
|
||||
</Request>
|
||||
</Requests>
|
||||
````
|
||||
|
||||
Please see [Tabs](/docs/ui/components/tabs) for advanced usage information.
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
title: Responses
|
||||
description: Components for response examples.
|
||||
---
|
||||
|
||||
import {
|
||||
Responses,
|
||||
Response,
|
||||
ResponseTypes,
|
||||
ExampleResponse,
|
||||
TypeScriptResponse,
|
||||
} from 'fumadocs-ui/components/api';
|
||||
|
||||
<Responses items={["200"]}>
|
||||
|
||||
<Response value="200">
|
||||
|
||||
Description
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "3be6453c-03eb-4357-ae5a-984a0e574a54",
|
||||
"name": "Pirate Coding Workshop"
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
export interface Response {
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
</Responses>
|
||||
|
||||
## Usage
|
||||
|
||||
Import it in your MDX documents.
|
||||
|
||||
````mdx title="tickets.mdx"
|
||||
import {
|
||||
Responses,
|
||||
Response,
|
||||
ResponseTypes,
|
||||
ExampleResponse,
|
||||
TypeScriptResponse,
|
||||
} from 'fumadocs-ui/components/api';
|
||||
|
||||
<Responses items={["200"]}>
|
||||
<Response value="200">
|
||||
|
||||
Description
|
||||
|
||||
<ResponseTypes>
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"eventId": "3be6453c-03eb-4357-ae5a-984a0e574a54",
|
||||
"name": "Pirate Coding Workshop",
|
||||
"location": "Computer Room",
|
||||
"eventDescription": "Captain Blackbeard shares his love of the C...language. And possibly Arrrrr (R lang).",
|
||||
"dates": ["2023-10-29"],
|
||||
"price": 25
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
export interface Response {
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
</ResponseTypes>
|
||||
</Response>
|
||||
</Responses>
|
||||
````
|
||||
|
||||
Please see [Tabs](/docs/ui/components/tabs) and [Accordion](/docs/ui/components/accordion) for advanced usage information.
|
||||
@@ -11,13 +11,35 @@ Install the required packages.
|
||||
fumadocs-openapi
|
||||
```
|
||||
|
||||
### Generate Styles
|
||||
|
||||
The interactive UI of OpenAPI integration is styled with Tailwind CSS, it doesn't include a pre-built stylesheet by default.
|
||||
You must use it in conjunction with the [official Tailwind CSS plugin](/docs/ui/theme#docsui-plugin).
|
||||
|
||||
Add the package to `content` under your Tailwind CSS configuration.
|
||||
|
||||
```js title="tailwind.config.js"
|
||||
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./node_modules/fumadocs-ui/dist/**/*.js',
|
||||
'./node_modules/fumadocs-openapi/dist/**/*.js',
|
||||
],
|
||||
presets: [createPreset()],
|
||||
};
|
||||
```
|
||||
|
||||
### Generate Files
|
||||
|
||||
Create a script:
|
||||
|
||||
```js title="scripts/generate-docs.mjs"
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
|
||||
void generateFiles({
|
||||
input: ['./petstore.yaml'],
|
||||
input: ['./petstore.yaml'], // the OpenAPI schemas
|
||||
output: './content/docs',
|
||||
});
|
||||
```
|
||||
@@ -34,6 +56,16 @@ node ./scripts/generate-docs.mjs
|
||||
|
||||
Fumadocs OpenAPI generates MDX content directly, you can use a formatter (e.g. Prettier) to format the output files.
|
||||
|
||||
### Features
|
||||
|
||||
The official OpenAPI integration supports:
|
||||
|
||||
- Basic API endpoint information
|
||||
- Interactive API playground
|
||||
- Example code to send request (in different programming languages)
|
||||
- Response samples and TypeScript definitions
|
||||
- Request parameters and body generated from schemas
|
||||
|
||||
### Demo
|
||||
|
||||
[View demo](/docs/ui/museum).
|
||||
@@ -156,10 +188,10 @@ void generateFiles({
|
||||
input: ['./petstore.yaml'],
|
||||
output: './content/docs',
|
||||
renderer: {
|
||||
Root(child) {
|
||||
Root(props, child) {
|
||||
return createElement(
|
||||
'Root',
|
||||
{},
|
||||
props,
|
||||
'<div className="bg-secondary p-4 rounded-lg">Demo Only</div>',
|
||||
...child,
|
||||
);
|
||||
|
||||
@@ -8,9 +8,9 @@ info:
|
||||
url: 'https://redocly.com/docs/cli/'
|
||||
license:
|
||||
name: MIT
|
||||
url: 'https://opensource.org/license/mit/ '
|
||||
url: 'https://opensource.org/license/mit/'
|
||||
servers:
|
||||
- url: 'https://api.fake-museum-example.com/v1'
|
||||
- url: 'https://redocly.com/_mock/docs/openapi/museum-api'
|
||||
paths:
|
||||
/museum-hours:
|
||||
get:
|
||||
@@ -366,6 +366,9 @@ components:
|
||||
$ref: '#/components/schemas/EventDates'
|
||||
price:
|
||||
$ref: '#/components/schemas/EventPrice'
|
||||
meta:
|
||||
type: 'object'
|
||||
additionalProperties: true
|
||||
required:
|
||||
- name
|
||||
- location
|
||||
|
||||
@@ -7,10 +7,10 @@ void OpenAPI.generateFiles({
|
||||
output: './content/docs/ui',
|
||||
per: 'tag',
|
||||
renderer: {
|
||||
Root(child) {
|
||||
Root(props, child) {
|
||||
return OpenAPI.createElement(
|
||||
'Root',
|
||||
{},
|
||||
props,
|
||||
'<div className="bg-secondary p-4 rounded-lg">Demo Only</div>',
|
||||
...child,
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
'./content/**/*.mdx',
|
||||
'./mdx-components.tsx',
|
||||
'./node_modules/fumadocs-ui/dist/**/*.js',
|
||||
'./node_modules/fumadocs-openapi/dist/**/*.js',
|
||||
],
|
||||
presets: [
|
||||
createPreset({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['custom/library'],
|
||||
extends: ['custom/next'],
|
||||
rules: {
|
||||
// commonjs compatibility
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,8 +11,28 @@
|
||||
"license": "MIT",
|
||||
"author": "Fuma Nama",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./ui": {
|
||||
"import": "./dist/ui/index.js",
|
||||
"types": "./dist/ui/index.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./dist/index.d.ts"
|
||||
],
|
||||
"ui": [
|
||||
"./dist/ui/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -25,19 +45,34 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^11.6.4",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"fast-glob": "^3.3.1",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema-to-typescript": "^14.1.0",
|
||||
"openapi-sampler": "^1.5.1"
|
||||
"lucide-react": "^0.400.0",
|
||||
"openapi-sampler": "^1.5.1",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"shiki": "^1.10.3",
|
||||
"swr": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/openapi-sampler": "^1.0.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"next": "^14.2.4",
|
||||
"openapi-types": "^12.1.3",
|
||||
"tsconfig": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">= 14.1.0",
|
||||
"react": ">= 18",
|
||||
"react-dom": ">= 18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Parser from '@apidevtools/json-schema-ref-parser';
|
||||
import type { OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import { buildRoutes } from '@/build-routes';
|
||||
import { renderPage } from '@/render/page';
|
||||
import type { Endpoint } from '@/endpoint';
|
||||
import { generateDocument } from '@/utils/generate-document';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import type { RenderContext } from './types';
|
||||
import { defaultRenderer, type Renderer } from './render/renderer';
|
||||
import { type CodeSample, renderOperation } from './render/operation';
|
||||
@@ -61,10 +62,10 @@ export async function generate(
|
||||
}
|
||||
}
|
||||
|
||||
return renderPage(
|
||||
return generateDocument(
|
||||
document.info.title,
|
||||
document.info.description,
|
||||
child,
|
||||
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
|
||||
options,
|
||||
);
|
||||
}
|
||||
@@ -80,10 +81,12 @@ export async function generateOperations(
|
||||
return await Promise.all(
|
||||
routes.flatMap<Promise<GenerateOperationOutput>>((route) => {
|
||||
return route.methods.map(async (method) => {
|
||||
const content = renderPage(
|
||||
const content = generateDocument(
|
||||
method.summary ?? method.method,
|
||||
method.description,
|
||||
[await renderOperation(route.path, method, ctx, true)],
|
||||
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, [
|
||||
await renderOperation(route.path, method, ctx, true),
|
||||
]),
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -122,7 +125,12 @@ export async function generateTags(
|
||||
|
||||
return {
|
||||
tag,
|
||||
content: renderPage(tag, info?.description, child, options),
|
||||
content: generateDocument(
|
||||
idToTitle(tag),
|
||||
info?.description,
|
||||
ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
|
||||
options,
|
||||
),
|
||||
} satisfies GenerateTagOutput;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,9 @@ import * as JS from '@/requests/javascript';
|
||||
import { type MethodInformation, type RenderContext } from '@/types';
|
||||
import { noRef, getPreferredMedia } from '@/utils/schema';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { getScheme } from '@/utils/get-security';
|
||||
import { renderPlayground } from '@/render/playground';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { heading, p } from './element';
|
||||
import { schemaElement } from './schema';
|
||||
|
||||
@@ -31,9 +34,14 @@ export async function renderOperation(
|
||||
const info: string[] = [];
|
||||
const example: string[] = [];
|
||||
|
||||
const title = method.summary ?? method.operationId;
|
||||
if (title && !noTitle) {
|
||||
info.push(heading(level, title));
|
||||
if (!noTitle) {
|
||||
info.push(
|
||||
heading(
|
||||
level,
|
||||
method.summary ??
|
||||
(method.operationId ? idToTitle(method.operationId) : path),
|
||||
),
|
||||
);
|
||||
level++;
|
||||
}
|
||||
if (method.description) info.push(p(method.description));
|
||||
@@ -104,6 +112,7 @@ export async function renderOperation(
|
||||
}
|
||||
|
||||
info.push(getResponseTable(method));
|
||||
info.push(renderPlayground(path, method, ctx));
|
||||
|
||||
const samples: CodeSample[] = dedupe([
|
||||
{
|
||||
@@ -163,14 +172,10 @@ function getAuthSection(
|
||||
): string {
|
||||
const info: string[] = [];
|
||||
|
||||
const schemas = document.components?.securitySchemes ?? {};
|
||||
for (const requirement of requirements) {
|
||||
if (info.length > 0) info.push(`---`);
|
||||
|
||||
for (const [name, scopes] of Object.entries(requirement)) {
|
||||
if (!(name in schemas)) continue;
|
||||
const schema = noRef(schemas[name]);
|
||||
|
||||
for (const schema of getScheme(requirement, document)) {
|
||||
if (schema.type === 'http') {
|
||||
info.push(
|
||||
renderer.Property(
|
||||
@@ -199,7 +204,7 @@ function getAuthSection(
|
||||
[
|
||||
p(schema.description),
|
||||
`In: \`header\``,
|
||||
`Scope: \`${scopes.length > 0 ? scopes.join(', ') : 'none'}\``,
|
||||
`Scope: \`${schema.scopes.length > 0 ? schema.scopes.join(', ') : 'none'}\``,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import type { OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import { getPreferredMedia, noRef } from '@/utils/schema';
|
||||
import { getScheme } from '@/utils/get-security';
|
||||
|
||||
interface BaseRequestField {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface BaseSchema {
|
||||
description?: string;
|
||||
isRequired: boolean;
|
||||
}
|
||||
|
||||
export type PrimitiveRequestField = BaseRequestField & PrimitiveSchema;
|
||||
|
||||
interface PrimitiveSchema extends BaseSchema {
|
||||
type: 'boolean' | 'string' | 'number';
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export interface ReferenceSchema extends BaseSchema {
|
||||
type: 'ref';
|
||||
schema: string;
|
||||
}
|
||||
|
||||
interface ArraySchema extends BaseSchema {
|
||||
type: 'array';
|
||||
/**
|
||||
* Reference to item schema or the schema
|
||||
*/
|
||||
items: string | RequestSchema;
|
||||
}
|
||||
|
||||
interface ObjectSchema extends BaseSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, ReferenceSchema>;
|
||||
|
||||
/**
|
||||
* Reference to schema, or true if it's `any`
|
||||
*/
|
||||
additionalProperties?: boolean | string;
|
||||
}
|
||||
|
||||
interface SwitcherSchema extends BaseSchema {
|
||||
type: 'switcher';
|
||||
items: Record<string, ReferenceSchema | RequestSchema>;
|
||||
}
|
||||
|
||||
interface NullSchema extends BaseSchema {
|
||||
type: 'null';
|
||||
}
|
||||
|
||||
export type RequestSchema =
|
||||
| PrimitiveSchema
|
||||
| ArraySchema
|
||||
| ObjectSchema
|
||||
| SwitcherSchema
|
||||
| NullSchema;
|
||||
|
||||
interface Context {
|
||||
schema: Record<string, RequestSchema>;
|
||||
registered: WeakMap<OpenAPI.SchemaObject, string>;
|
||||
nextId: () => string;
|
||||
}
|
||||
|
||||
export interface APIPlaygroundProps {
|
||||
route: string;
|
||||
method: string;
|
||||
authorization?: PrimitiveRequestField;
|
||||
path?: PrimitiveRequestField[];
|
||||
query?: PrimitiveRequestField[];
|
||||
header?: PrimitiveRequestField[];
|
||||
body?: RequestSchema;
|
||||
schemas: Record<string, RequestSchema>;
|
||||
}
|
||||
|
||||
export function renderPlayground(
|
||||
path: string,
|
||||
method: MethodInformation,
|
||||
ctx: RenderContext,
|
||||
): string {
|
||||
let currentId = 0;
|
||||
const context: Context = {
|
||||
schema: {},
|
||||
nextId() {
|
||||
return String(currentId++);
|
||||
},
|
||||
registered: new WeakMap(),
|
||||
};
|
||||
const body = method.requestBody
|
||||
? getPreferredMedia(noRef(method.requestBody).content)
|
||||
: undefined;
|
||||
|
||||
return ctx.renderer.APIPlayground({
|
||||
authorization: getAuthorizationField(method, ctx),
|
||||
method: method.method,
|
||||
route: path,
|
||||
path: method.parameters
|
||||
.filter((v) => v.in === 'path')
|
||||
.map((v) => parameterToField(v, context)),
|
||||
query: method.parameters
|
||||
.filter((v) => v.in === 'query')
|
||||
.map((v) => parameterToField(v, context)),
|
||||
header: method.parameters
|
||||
.filter((v) => v.in === 'header')
|
||||
.map((v) => parameterToField(v, context)),
|
||||
body: body?.schema
|
||||
? toSchema(noRef(body.schema), true, context)
|
||||
: undefined,
|
||||
schemas: context.schema,
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizationField(
|
||||
method: MethodInformation,
|
||||
ctx: RenderContext,
|
||||
): PrimitiveRequestField | undefined {
|
||||
const security = method.security ?? ctx.document.security ?? [];
|
||||
if (security.length === 0) return;
|
||||
const singular = security.find(
|
||||
(requirements) => Object.keys(requirements).length === 1,
|
||||
);
|
||||
if (!singular) return;
|
||||
|
||||
const scheme = getScheme(singular, ctx.document)[0];
|
||||
|
||||
return {
|
||||
type: 'string',
|
||||
name: 'Authorization',
|
||||
defaultValue:
|
||||
scheme.type === 'oauth2' ||
|
||||
(scheme.type === 'http' && scheme.scheme === 'bearer')
|
||||
? 'Bearer'
|
||||
: 'Basic',
|
||||
isRequired: security.every(
|
||||
(requirements) => Object.keys(requirements).length > 0,
|
||||
),
|
||||
description: 'The Authorization access token',
|
||||
};
|
||||
}
|
||||
|
||||
function getIdFromSchema(
|
||||
schema: OpenAPI.SchemaObject,
|
||||
required: boolean,
|
||||
ctx: Context,
|
||||
): string {
|
||||
const registered = ctx.registered.get(schema);
|
||||
|
||||
if (registered === undefined) {
|
||||
const id = ctx.nextId();
|
||||
ctx.registered.set(schema, id);
|
||||
ctx.schema[id] = toSchema(schema, required, ctx);
|
||||
return id;
|
||||
}
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
function parameterToField(
|
||||
v: OpenAPI.ParameterObject,
|
||||
ctx: Context,
|
||||
): PrimitiveRequestField {
|
||||
return {
|
||||
name: v.name,
|
||||
...(toSchema(
|
||||
noRef(v.schema) ?? { type: 'string' },
|
||||
v.required ?? false,
|
||||
ctx,
|
||||
) as PrimitiveSchema),
|
||||
};
|
||||
}
|
||||
|
||||
function toReference(
|
||||
schema: OpenAPI.SchemaObject,
|
||||
required: boolean,
|
||||
ctx: Context,
|
||||
): ReferenceSchema {
|
||||
return {
|
||||
type: 'ref',
|
||||
isRequired: required,
|
||||
schema: getIdFromSchema(schema, false, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
function toSchema(
|
||||
schema: OpenAPI.SchemaObject,
|
||||
required: boolean,
|
||||
ctx: Context,
|
||||
): RequestSchema {
|
||||
if (schema.type === 'array') {
|
||||
return {
|
||||
type: 'array',
|
||||
description: schema.description ?? schema.title,
|
||||
isRequired: required,
|
||||
items: getIdFromSchema(noRef(schema.items), false, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
schema.type === 'object' ||
|
||||
schema.properties !== undefined ||
|
||||
schema.allOf !== undefined
|
||||
) {
|
||||
const properties: Record<string, ReferenceSchema> = {};
|
||||
|
||||
Object.entries(schema.properties ?? {}).forEach(([key, prop]) => {
|
||||
properties[key] = toReference(
|
||||
noRef(prop),
|
||||
schema.required?.includes(key) ?? false,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
schema.allOf?.forEach((c) => {
|
||||
const field = toSchema(noRef(c), true, ctx);
|
||||
|
||||
if (field.type === 'object') Object.assign(properties, field.properties);
|
||||
});
|
||||
|
||||
const additional = noRef(schema.additionalProperties);
|
||||
let additionalProperties: string | boolean | undefined;
|
||||
|
||||
if (additional && typeof additional === 'object') {
|
||||
if (
|
||||
!additional.type &&
|
||||
!additional.anyOf &&
|
||||
!additional.allOf &&
|
||||
!additional.oneOf
|
||||
) {
|
||||
additionalProperties = true;
|
||||
} else {
|
||||
additionalProperties = getIdFromSchema(additional, false, ctx);
|
||||
}
|
||||
} else {
|
||||
additionalProperties = additional;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
isRequired: required,
|
||||
description: schema.description ?? schema.title,
|
||||
properties,
|
||||
additionalProperties,
|
||||
};
|
||||
}
|
||||
|
||||
if (schema.type === undefined) {
|
||||
const combine = schema.anyOf ?? schema.oneOf;
|
||||
|
||||
if (combine) {
|
||||
return {
|
||||
type: 'switcher',
|
||||
description: schema.description ?? schema.title,
|
||||
items: Object.fromEntries(
|
||||
combine.map((c, idx) => {
|
||||
const item = noRef(c);
|
||||
|
||||
return [
|
||||
item.title ?? item.type ?? `Item ${idx.toString()}`,
|
||||
toReference(item, true, ctx),
|
||||
];
|
||||
}),
|
||||
),
|
||||
isRequired: required,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'null',
|
||||
isRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: schema.type === 'integer' ? 'number' : schema.type,
|
||||
defaultValue: (schema.example ?? '') as string,
|
||||
isRequired: required,
|
||||
description: schema.description ?? schema.title,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { APIPlaygroundProps } from '@/render/playground';
|
||||
import { codeblock, createElement } from './element';
|
||||
|
||||
export interface ResponsesProps {
|
||||
@@ -30,8 +31,12 @@ export interface RequestProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface RootProps {
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface Renderer {
|
||||
Root: (child: string[]) => string;
|
||||
Root: (props: RootProps, child: string[]) => string;
|
||||
API: (child: string[]) => string;
|
||||
APIInfo: (props: APIInfoProps, child: string[]) => string;
|
||||
APIExample: (child: string[]) => string;
|
||||
@@ -51,10 +56,18 @@ export interface Renderer {
|
||||
*/
|
||||
ObjectCollapsible: (props: ObjectCollapsibleProps, child: string[]) => string;
|
||||
Property: (props: PropertyProps, child: string[]) => string;
|
||||
APIPlayground: (props: APIPlaygroundProps) => string;
|
||||
}
|
||||
|
||||
export type {
|
||||
APIPlaygroundProps,
|
||||
RequestSchema,
|
||||
PrimitiveRequestField,
|
||||
ReferenceSchema,
|
||||
} from '@/render/playground';
|
||||
|
||||
export const defaultRenderer: Renderer = {
|
||||
Root: (child) => createElement('Root', {}, ...child),
|
||||
Root: (props, child) => createElement('Root', props, ...child),
|
||||
API: (child) => createElement('API', {}, ...child),
|
||||
APIInfo: (props, child) => createElement('APIInfo', props, ...child),
|
||||
APIExample: (child) => createElement('APIExample', {}, ...child),
|
||||
@@ -76,4 +89,5 @@ export const defaultRenderer: Renderer = {
|
||||
Requests: (items, child) => createElement('Requests', { items }, ...child),
|
||||
Request: ({ language, code, name }) =>
|
||||
createElement('Request', { value: name }, codeblock({ language }, code)),
|
||||
APIPlayground: (props) => createElement('APIPlayground', props),
|
||||
};
|
||||
|
||||
@@ -31,7 +31,11 @@ interface Context {
|
||||
}
|
||||
|
||||
function isObject(schema: OpenAPI.SchemaObject): boolean {
|
||||
return schema.type === 'object' || schema.properties !== undefined;
|
||||
return (
|
||||
schema.type === 'object' ||
|
||||
schema.properties !== undefined ||
|
||||
schema.additionalProperties !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function schemaElement(
|
||||
@@ -122,9 +126,19 @@ function render(
|
||||
}),
|
||||
]),
|
||||
);
|
||||
} else if (schema.allOf) {
|
||||
child.push(
|
||||
renderer.ObjectCollapsible({ name }, [
|
||||
render(name, combineSchema(schema.allOf.map(noRef)), {
|
||||
...ctx,
|
||||
parseObject: true,
|
||||
required: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
const mentionedObjectTypes = [
|
||||
...(schema.anyOf ?? schema.oneOf ?? schema.allOf ?? []),
|
||||
...(schema.anyOf ?? schema.oneOf ?? []),
|
||||
...(schema.not ? [schema.not] : []),
|
||||
...(schema.type === 'array' ? [schema.items] : []),
|
||||
]
|
||||
@@ -160,6 +174,41 @@ function render(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple object schemas into one
|
||||
*/
|
||||
function combineSchema(schema: OpenAPI.SchemaObject[]): OpenAPI.SchemaObject {
|
||||
const result: OpenAPI.SchemaObject = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
function add(s: OpenAPI.SchemaObject): void {
|
||||
result.properties ??= {};
|
||||
|
||||
if (s.properties) {
|
||||
Object.assign(result.properties, s.properties);
|
||||
}
|
||||
|
||||
result.additionalProperties ??= {};
|
||||
if (s.additionalProperties === true) {
|
||||
result.additionalProperties = true;
|
||||
} else if (
|
||||
s.additionalProperties &&
|
||||
typeof result.additionalProperties !== 'boolean'
|
||||
) {
|
||||
Object.assign(result.additionalProperties, s.additionalProperties);
|
||||
}
|
||||
|
||||
if (s.allOf) {
|
||||
add(combineSchema(s.allOf.map(noRef)));
|
||||
}
|
||||
}
|
||||
|
||||
schema.forEach(add);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the schema needs another collapsible to explain
|
||||
*/
|
||||
@@ -198,7 +247,6 @@ function getSchemaType(schema: OpenAPI.SchemaObject): string {
|
||||
|
||||
if (schema.type) return schema.type;
|
||||
|
||||
// object without specified type
|
||||
if (isObject(schema)) return 'object';
|
||||
|
||||
return 'unknown';
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
type ButtonHTMLAttributes,
|
||||
Fragment,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { cn, useCopyButton, buttonVariants } from 'fumadocs-ui/components/api';
|
||||
import { badgeVariants, getBadgeColor } from '@/ui/shared';
|
||||
import { ApiProvider, useApiContext } from '@/ui/contexts/api';
|
||||
|
||||
export interface RootProps extends HTMLAttributes<HTMLDivElement> {
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
className,
|
||||
...props
|
||||
}: RootProps): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-24 text-sm text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider defaultBaseUrl={baseUrl}>{children}</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function API({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-2 xl:flex-row xl:items-start',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface APIInfoProps extends HTMLAttributes<HTMLDivElement> {
|
||||
route: string;
|
||||
method?: string;
|
||||
badgeClassName?: string;
|
||||
}
|
||||
|
||||
function Route({ route }: { route: string }): ReactNode {
|
||||
const segments = route.split('/').filter((part) => part.length > 0);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-1 text-sm">
|
||||
{segments.map((part, index) => (
|
||||
<Fragment key={index}>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span className="text-foreground">{part}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIInfo({
|
||||
children,
|
||||
className,
|
||||
route,
|
||||
badgeClassName,
|
||||
method = 'GET',
|
||||
...props
|
||||
}: APIInfoProps): React.ReactElement {
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1 prose-no-margin', className)} {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
'sticky top-24 z-10 flex flex-row items-center gap-2 rounded-lg border bg-card p-3 md:top-10',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
badgeVariants({ color: getBadgeColor(method) }),
|
||||
badgeClassName,
|
||||
)}
|
||||
>
|
||||
{method}
|
||||
</span>
|
||||
<Route route={route} />
|
||||
|
||||
<CopyRouteButton className="ms-auto size-6 p-1" route={route} />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PropertyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
deprecated: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Property({
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
deprecated,
|
||||
children,
|
||||
}: PropertyProps): React.ReactElement {
|
||||
return (
|
||||
<div className="mb-4 flex flex-col rounded-lg border bg-card p-3 prose-no-margin">
|
||||
<h4 className="inline-flex items-center gap-4">
|
||||
<code>{name}</code>
|
||||
{required ? (
|
||||
<div className={cn(badgeVariants({ color: 'red' }))}>Required</div>
|
||||
) : null}
|
||||
{deprecated ? (
|
||||
<div className={cn(badgeVariants({ color: 'yellow' }))}>
|
||||
Deprecated
|
||||
</div>
|
||||
) : null}
|
||||
<span className="ms-auto font-mono text-[13px] text-muted-foreground">
|
||||
{type}
|
||||
</span>
|
||||
</h4>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExample({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn('sticky top-10 prose-no-margin xl:w-[400px]', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResponseTypes(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="!-m-4 border-none pt-2"
|
||||
defaultValue="Response"
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Response">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function TypeScriptResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Typescript">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function ObjectCollapsible(props: {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions type="single">
|
||||
<Accordion title={props.name}>{props.children}</Accordion>
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyRouteButton({
|
||||
className,
|
||||
route,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
route: string;
|
||||
}): React.ReactElement {
|
||||
const { baseUrl } = useApiContext();
|
||||
|
||||
const [checked, onCopy] = useCopyButton(() => {
|
||||
void navigator.clipboard.writeText(`${baseUrl ?? ''}${route}`);
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
onClick={onCopy}
|
||||
{...props}
|
||||
>
|
||||
{checked ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type HTMLAttributes, useLayoutEffect, useState } from 'react';
|
||||
import * as Base from 'fumadocs-ui/components/codeblock';
|
||||
import { useApiContext } from '@/ui/contexts/api';
|
||||
|
||||
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
|
||||
code: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
export function CodeBlock({
|
||||
code,
|
||||
lang = 'json',
|
||||
...props
|
||||
}: CodeBlockProps): React.ReactElement {
|
||||
const { highlighter } = useApiContext();
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!highlighter) return;
|
||||
|
||||
const themedHtml = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
defaultColor: false,
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
});
|
||||
|
||||
setHtml(themedHtml);
|
||||
}, [code, lang, highlighter]);
|
||||
|
||||
return (
|
||||
<Base.CodeBlock className="my-0">
|
||||
<Base.Pre {...props} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Base.CodeBlock>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import {
|
||||
Controller,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { createContext, forwardRef, useContext, useId } from 'react';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
interface FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
name: TName;
|
||||
}
|
||||
|
||||
const FormFieldContext = createContext<FormFieldContextValue>({
|
||||
name: '',
|
||||
});
|
||||
|
||||
interface FormItemContextValue {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const FormItemContext = createContext<FormItemContextValue>({ id: '' });
|
||||
|
||||
function FormField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({ ...props }: ControllerProps<TFieldValues, TName>): React.ReactElement {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useFormField(): {
|
||||
id: string;
|
||||
name: string;
|
||||
formItemId: string;
|
||||
formDescriptionId: string;
|
||||
isError: boolean;
|
||||
} {
|
||||
const fieldContext = useContext(FormFieldContext);
|
||||
const { id } = useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
isError: Boolean(fieldState.error),
|
||||
};
|
||||
}
|
||||
|
||||
const FormItem = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
export const labelVariants = cva(
|
||||
'text-xs font-medium text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const FormLabel = forwardRef<
|
||||
HTMLLabelElement,
|
||||
React.LabelHTMLAttributes<HTMLLabelElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { isError, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), isError && 'text-red-500', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = 'FormLabel';
|
||||
|
||||
const FormControl = forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { isError, formItemId, formDescriptionId } = useFormField();
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={formDescriptionId}
|
||||
aria-invalid={isError}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = 'FormControl';
|
||||
|
||||
const FormDescription = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-xs text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = 'FormDescription';
|
||||
|
||||
export { Form, FormItem, FormLabel, FormControl, FormDescription, FormField };
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm text-foreground transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,147 @@
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 items-center justify-between rounded-md border px-3 py-2 text-sm text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn('flex items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn('flex items-center justify-center py-1', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-popover-out data-[state=open]:animate-popover-in',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('py-1.5 pe-2 ps-6 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex select-none flex-row items-center rounded-md py-1.5 pe-2 ps-6 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute start-2">
|
||||
<Check className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { HighlighterCore } from 'shiki/core';
|
||||
|
||||
export interface ApiProviderProps {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
*/
|
||||
defaultBaseUrl?: string;
|
||||
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ApiContextType {
|
||||
baseUrl?: string;
|
||||
setBaseUrl: (value: string) => void;
|
||||
highlighter: HighlighterCore | null;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContextType>({
|
||||
baseUrl: undefined,
|
||||
setBaseUrl: () => undefined,
|
||||
highlighter: null,
|
||||
});
|
||||
|
||||
export function useApiContext(): ApiContextType {
|
||||
return useContext(ApiContext);
|
||||
}
|
||||
|
||||
async function initHighlighter(): Promise<HighlighterCore> {
|
||||
const { createHighlighterCore } = await import('shiki/core');
|
||||
const getWasm = await import('shiki/wasm');
|
||||
|
||||
return createHighlighterCore({
|
||||
themes: [
|
||||
import('shiki/themes/github-light.mjs'),
|
||||
import('shiki/themes/github-dark.mjs'),
|
||||
],
|
||||
langs: [import('shiki/langs/json.mjs')],
|
||||
loadWasm: getWasm,
|
||||
});
|
||||
}
|
||||
|
||||
let highlighterInstance: HighlighterCore | undefined;
|
||||
|
||||
export function ApiProvider({
|
||||
defaultBaseUrl,
|
||||
children,
|
||||
}: ApiProviderProps): React.ReactElement {
|
||||
const [highlighter, setHighlighter] = useState<HighlighterCore | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl((prev) => localStorage.getItem('apiBaseUrl') ?? prev);
|
||||
if (highlighterInstance) setHighlighter(highlighterInstance);
|
||||
else
|
||||
void initHighlighter().then((res) => {
|
||||
setHighlighter(res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (baseUrl) localStorage.setItem('apiBaseUrl', baseUrl);
|
||||
}, [baseUrl]);
|
||||
|
||||
return (
|
||||
<ApiContext.Provider value={{ baseUrl, setBaseUrl, highlighter }}>
|
||||
{children}
|
||||
</ApiContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createContext, type MutableRefObject, useContext } from 'react';
|
||||
import { type ReferenceSchema, type RequestSchema } from '@/render/playground';
|
||||
|
||||
interface SchemaContextType {
|
||||
references: Record<string, RequestSchema>;
|
||||
dynamic: MutableRefObject<Map<string, DynamicField>>;
|
||||
}
|
||||
|
||||
export type DynamicField =
|
||||
| {
|
||||
type: 'object';
|
||||
properties: string[];
|
||||
}
|
||||
| {
|
||||
type: 'field';
|
||||
schema: RequestSchema | ReferenceSchema;
|
||||
};
|
||||
|
||||
export const SchemaContext = createContext<SchemaContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useSchemaContext(): SchemaContextType {
|
||||
const ctx = useContext(SchemaContext);
|
||||
if (!ctx) throw new Error('Missing provider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { CircleCheckIcon, CircleXIcon } from 'lucide-react';
|
||||
import { resolve } from '@/ui/shared';
|
||||
import { type RequestSchema } from '@/render/playground';
|
||||
import { type DynamicField } from '@/ui/contexts/schema';
|
||||
|
||||
/**
|
||||
* Create request body from value
|
||||
*/
|
||||
export function createBodyFromValue(
|
||||
value: unknown,
|
||||
schema: RequestSchema,
|
||||
references: Record<string, RequestSchema>,
|
||||
dynamic: Map<string, DynamicField>,
|
||||
): unknown {
|
||||
return convertValue('body', value, schema, references, dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value (object or string) to the corresponding type of schema
|
||||
*
|
||||
* @param fieldName - field name of value
|
||||
* @param value - the original value
|
||||
* @param schema - the schema of field
|
||||
* @param references - schema references
|
||||
* @param dynamic - Dynamic references
|
||||
*/
|
||||
function convertValue(
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
schema: RequestSchema,
|
||||
references: Record<string, RequestSchema>,
|
||||
dynamic: Map<string, DynamicField>,
|
||||
): unknown {
|
||||
const isEmpty = value === '' || value === undefined || value === null;
|
||||
if (isEmpty && schema.isRequired)
|
||||
return schema.type === 'boolean' ? false : '';
|
||||
else if (isEmpty) return undefined;
|
||||
|
||||
if (Array.isArray(value) && schema.type === 'array') {
|
||||
return value.map((item: unknown, index) =>
|
||||
convertValue(
|
||||
`${fieldName}.${String(index)}`,
|
||||
item,
|
||||
resolve(schema.items, references),
|
||||
references,
|
||||
dynamic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'switcher') {
|
||||
return convertDynamicValue(fieldName, value, references, dynamic);
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && schema.type === 'object') {
|
||||
const entries = Object.keys(value).map((key) => {
|
||||
const prop = value[key as keyof object];
|
||||
const propFieldName = `${fieldName}.${key}`;
|
||||
|
||||
if (key in schema.properties) {
|
||||
return [
|
||||
key,
|
||||
convertValue(
|
||||
propFieldName,
|
||||
prop,
|
||||
resolve(schema.properties[key], references),
|
||||
references,
|
||||
dynamic,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (schema.additionalProperties) {
|
||||
return [
|
||||
key,
|
||||
convertDynamicValue(propFieldName, prop, references, dynamic),
|
||||
];
|
||||
}
|
||||
|
||||
console.warn('Could not resolve field', propFieldName, dynamic);
|
||||
return [key, prop];
|
||||
});
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case 'number':
|
||||
return Number(value);
|
||||
case 'boolean':
|
||||
return value === 'null' ? undefined : value === 'true';
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function convertDynamicValue(
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
references: Record<string, RequestSchema>,
|
||||
dynamic: Map<string, DynamicField>,
|
||||
): unknown {
|
||||
const fieldDynamic = dynamic.get(fieldName);
|
||||
|
||||
return convertValue(
|
||||
fieldName,
|
||||
value,
|
||||
fieldDynamic?.type === 'field'
|
||||
? resolve(fieldDynamic.schema, references)
|
||||
: { type: 'null', isRequired: false },
|
||||
references,
|
||||
dynamic,
|
||||
);
|
||||
}
|
||||
|
||||
interface StatusInfo {
|
||||
description: string;
|
||||
color: string;
|
||||
icon: React.ElementType;
|
||||
}
|
||||
|
||||
const statusMap: Record<number, StatusInfo> = {
|
||||
400: { description: 'Bad Request', color: 'text-red-500', icon: CircleXIcon },
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
color: 'text-red-500',
|
||||
icon: CircleXIcon,
|
||||
},
|
||||
403: { description: 'Forbidden', color: 'text-red-500', icon: CircleXIcon },
|
||||
404: {
|
||||
description: 'Not Found',
|
||||
color: 'text-muted-foreground',
|
||||
icon: CircleXIcon,
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
color: 'text-red-500',
|
||||
icon: CircleXIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export function getStatusInfo(status: number): StatusInfo {
|
||||
if (status in statusMap) {
|
||||
return statusMap[status];
|
||||
}
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
return {
|
||||
description: 'Successful',
|
||||
color: 'text-green-500',
|
||||
icon: CircleCheckIcon,
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 400) {
|
||||
return { description: 'Error', color: 'text-red-500', icon: CircleXIcon };
|
||||
}
|
||||
|
||||
return {
|
||||
description: 'No Description',
|
||||
color: 'text-muted-foreground',
|
||||
icon: CircleXIcon,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
export * from './components';
|
||||
export { APIPlayground } from './playground';
|
||||
|
||||
export const Responses = Tabs;
|
||||
export const Response = Tab;
|
||||
|
||||
export const Requests = Tabs;
|
||||
export const Request = Tab;
|
||||
@@ -0,0 +1,544 @@
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||
import { cn, buttonVariants, useOnChange } from 'fumadocs-ui/components/api';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import type { RequestSchema } from '@/render/playground';
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
labelVariants,
|
||||
} from '@/ui/components/form';
|
||||
import { getDefaultValue, resolve } from '@/ui/shared';
|
||||
import { Input } from '@/ui/components/input';
|
||||
import { useSchemaContext } from './contexts/schema';
|
||||
|
||||
interface RenderOptions {
|
||||
field: RequestSchema;
|
||||
fieldName: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function renderInner({ field, ...props }: RenderOptions): React.ReactNode {
|
||||
if (field.type === 'object')
|
||||
return (
|
||||
<ObjectInput
|
||||
field={field}
|
||||
{...props}
|
||||
className={cn('rounded-lg border bg-accent/20 p-3', props.className)}
|
||||
/>
|
||||
);
|
||||
if (field.type === 'switcher')
|
||||
return <Switcher inline field={field} {...props} />;
|
||||
if (field.type === 'array')
|
||||
return (
|
||||
<ArrayInput
|
||||
field={field}
|
||||
{...props}
|
||||
className={cn('rounded-lg border bg-background p-3', props.className)}
|
||||
/>
|
||||
);
|
||||
if (field.type === 'null') return null;
|
||||
|
||||
return <NormalInput field={field} {...props} />;
|
||||
}
|
||||
|
||||
interface InputProps<Type> {
|
||||
field: Extract<RequestSchema, { type: Type }>;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
interface InputContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
name?: string;
|
||||
required: boolean;
|
||||
type?: string;
|
||||
description?: string;
|
||||
|
||||
inline?: boolean;
|
||||
toolbar?: ReactNode;
|
||||
}
|
||||
|
||||
function InputContainer({
|
||||
toolbar,
|
||||
name,
|
||||
required,
|
||||
type,
|
||||
description,
|
||||
inline = false,
|
||||
...props
|
||||
}: InputContainerProps): React.ReactElement {
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-2', props.className)}>
|
||||
<div className={cn(labelVariants(), 'inline-flex items-center gap-1')}>
|
||||
{name}
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
<div className="flex-1" />
|
||||
{type ? (
|
||||
<code className="text-xs text-muted-foreground">{type}</code>
|
||||
) : null}
|
||||
{toolbar}
|
||||
</div>
|
||||
{!inline ? <p className="text-xs">{description}</p> : null}
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectInput({
|
||||
field,
|
||||
fieldName,
|
||||
...props
|
||||
}: InputProps<'object'> & HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
const { references } = useSchemaContext();
|
||||
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-4', props.className)}>
|
||||
{Object.entries(field.properties).map(([key, child]) => (
|
||||
<InputField
|
||||
key={key}
|
||||
name={key}
|
||||
field={resolve(child, references)}
|
||||
fieldName={`${fieldName}.${key}`}
|
||||
/>
|
||||
))}
|
||||
{field.additionalProperties ? (
|
||||
<AdditionalProperties
|
||||
fieldName={fieldName}
|
||||
type={field.additionalProperties}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdditionalProperties({
|
||||
fieldName,
|
||||
type,
|
||||
}: {
|
||||
fieldName: string;
|
||||
type: boolean | string;
|
||||
}): React.ReactElement {
|
||||
const { control, setValue } = useFormContext();
|
||||
const { references, dynamic } = useSchemaContext();
|
||||
const [nextName, setNextName] = useState('');
|
||||
const [properties, setProperties] = useState<string[]>(() => {
|
||||
const d = dynamic.current.get(`additional_${fieldName}`);
|
||||
if (d?.type === 'object') return d.properties;
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
useOnChange(properties, () => {
|
||||
dynamic.current.set(`additional_${fieldName}`, {
|
||||
type: 'object',
|
||||
properties,
|
||||
});
|
||||
});
|
||||
|
||||
const onAppend = useCallback(() => {
|
||||
const name = nextName.trim();
|
||||
if (name.length === 0) return;
|
||||
|
||||
setProperties((p) => {
|
||||
if (p.includes(name)) return p;
|
||||
|
||||
setValue(`${fieldName}.${name}`, '');
|
||||
setNextName('');
|
||||
return [...p, name];
|
||||
});
|
||||
}, [nextName]);
|
||||
|
||||
const types =
|
||||
typeof type === 'string'
|
||||
? resolveDynamicTypes(references[type], references)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{properties.map((item) => (
|
||||
<Switcher
|
||||
key={item}
|
||||
name={item}
|
||||
field={{
|
||||
type: 'switcher',
|
||||
items: types ?? anyFields,
|
||||
isRequired: false,
|
||||
}}
|
||||
fieldName={`${fieldName}.${item}`}
|
||||
toolbar={
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove Item"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
setProperties((p) => p.filter((prop) => prop !== item));
|
||||
control.unregister(`${fieldName}.${item}`);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-row gap-1">
|
||||
<Input
|
||||
value={nextName}
|
||||
placeholder="Enter Property Name"
|
||||
onChange={useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNextName(e.target.value);
|
||||
}, [])}
|
||||
onKeyDown={useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onAppend();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[onAppend],
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonVariants({ color: 'secondary' }))}
|
||||
onClick={onAppend}
|
||||
>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDynamicTypes(
|
||||
schema: RequestSchema,
|
||||
references: Record<string, RequestSchema>,
|
||||
): Record<string, RequestSchema> {
|
||||
if (schema.type !== 'switcher') return { [schema.type]: schema };
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(schema.items).map(([key, value]) => [
|
||||
key,
|
||||
resolve(value, references),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
const anyFields: Record<string, RequestSchema> = {
|
||||
string: {
|
||||
type: 'string',
|
||||
isRequired: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
boolean: {
|
||||
type: 'boolean',
|
||||
isRequired: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
number: {
|
||||
type: 'number',
|
||||
isRequired: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
object: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: true,
|
||||
isRequired: false,
|
||||
},
|
||||
};
|
||||
|
||||
anyFields.array = {
|
||||
type: 'array',
|
||||
isRequired: false,
|
||||
items: {
|
||||
type: 'switcher',
|
||||
isRequired: false,
|
||||
items: anyFields,
|
||||
},
|
||||
};
|
||||
|
||||
function Switcher({
|
||||
field,
|
||||
fieldName,
|
||||
...props
|
||||
}: InputProps<'switcher'> & Partial<InputContainerProps>): React.ReactElement {
|
||||
const { references, dynamic } = useSchemaContext();
|
||||
const items = Object.keys(field.items);
|
||||
const [value, setValue] = useState<string>(() => {
|
||||
const d = dynamic.current.get(fieldName);
|
||||
|
||||
if (d?.type === 'field') {
|
||||
// schemas are passed from server components, they shouldn't be re-constructed
|
||||
const cached = items.find((item) => d.schema === field.items[item]);
|
||||
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
return items[0];
|
||||
});
|
||||
|
||||
useOnChange(value, () => {
|
||||
if (!value) return;
|
||||
|
||||
dynamic.current.set(fieldName, {
|
||||
type: 'field',
|
||||
schema: field.items[value],
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<InputContainer
|
||||
required={field.isRequired}
|
||||
description={field.description}
|
||||
{...props}
|
||||
toolbar={
|
||||
<>
|
||||
<Select value={value} onValueChange={setValue}>
|
||||
<SelectTrigger className="h-auto p-1 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{props.toolbar}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{renderInner({
|
||||
field: resolve(field.items[value], references),
|
||||
fieldName,
|
||||
})}
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function InputField({
|
||||
field,
|
||||
fieldName,
|
||||
...props
|
||||
}: {
|
||||
field: RequestSchema;
|
||||
fieldName: string;
|
||||
} & Partial<InputContainerProps>): React.ReactNode {
|
||||
const { references } = useSchemaContext();
|
||||
|
||||
if (field.type === 'null') return null;
|
||||
|
||||
if (field.type === 'object') {
|
||||
return (
|
||||
<InputContainer
|
||||
required={field.isRequired}
|
||||
type={field.type}
|
||||
description={field.description}
|
||||
{...props}
|
||||
>
|
||||
<ObjectInput
|
||||
field={field}
|
||||
fieldName={fieldName}
|
||||
className="rounded-lg border bg-accent/20 p-3"
|
||||
/>
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'array') {
|
||||
return (
|
||||
<InputContainer
|
||||
required={field.isRequired}
|
||||
description={
|
||||
field.description ?? resolve(field.items, references).description
|
||||
}
|
||||
type="array"
|
||||
{...props}
|
||||
>
|
||||
<ArrayInput
|
||||
fieldName={fieldName}
|
||||
field={field}
|
||||
className="rounded-lg border bg-background p-3"
|
||||
/>
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'switcher') {
|
||||
return <Switcher field={field} fieldName={fieldName} {...props} />;
|
||||
}
|
||||
|
||||
const { toolbar, inline = false, name, ...rest } = props;
|
||||
|
||||
return (
|
||||
<NormalInput
|
||||
field={field}
|
||||
fieldName={fieldName}
|
||||
header={
|
||||
<>
|
||||
<FormLabel className="inline-flex items-center gap-1">
|
||||
{name}
|
||||
{field.isRequired ? <span className="text-red-500">*</span> : null}
|
||||
<code className="ms-auto text-xs text-muted-foreground">
|
||||
{field.type}
|
||||
</code>
|
||||
{toolbar}
|
||||
</FormLabel>
|
||||
{!inline ? (
|
||||
<FormDescription className="text-xs">
|
||||
{field.description}
|
||||
</FormDescription>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NormalInput({
|
||||
fieldName,
|
||||
header,
|
||||
field,
|
||||
...props
|
||||
}: InputProps<'string' | 'boolean' | 'number'> & {
|
||||
header?: React.ReactNode;
|
||||
}): React.ReactElement {
|
||||
const { control } = useFormContext();
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={fieldName}
|
||||
render={({ field: { value, onChange, ...restField } }) => (
|
||||
<FormItem {...props}>
|
||||
{header}
|
||||
<Select
|
||||
value={value as string}
|
||||
onValueChange={onChange}
|
||||
disabled={restField.disabled}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger {...restField}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">True</SelectItem>
|
||||
<SelectItem value="false">False</SelectItem>
|
||||
{field.isRequired ? null : (
|
||||
<SelectItem value="null">Null</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={fieldName}
|
||||
render={({ field: { value, ...restField } }) => (
|
||||
<FormItem {...props}>
|
||||
{header}
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter value"
|
||||
type={field.type === 'string' ? 'text' : 'number'}
|
||||
value={value as string}
|
||||
{...restField}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrayInput({
|
||||
fieldName,
|
||||
field,
|
||||
...props
|
||||
}: {
|
||||
fieldName: string;
|
||||
field: {
|
||||
description?: string;
|
||||
items: RequestSchema | string;
|
||||
};
|
||||
} & HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
const { references } = useSchemaContext();
|
||||
const items = resolve(field.items, references);
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
const handleAppend = useCallback(() => {
|
||||
append(getDefaultValue(items, references));
|
||||
}, [append, references, items]);
|
||||
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-4', props.className)}>
|
||||
{fields.map((item, index) => (
|
||||
<InputField
|
||||
key={item.id}
|
||||
inline
|
||||
name={`Item ${String(index + 1)}`}
|
||||
field={items}
|
||||
fieldName={`${fieldName}.${String(index)}`}
|
||||
className="flex-1"
|
||||
toolbar={
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove Item"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
}),
|
||||
)}
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
buttonVariants({ color: 'secondary', className: 'gap-1.5' }),
|
||||
)}
|
||||
onClick={handleAppend}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
New Item
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { type HTMLAttributes, useMemo, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { cn, buttonVariants } from 'fumadocs-ui/components/api';
|
||||
import { useApiContext } from '@/ui/contexts/api';
|
||||
import { Form } from '@/ui/components/form';
|
||||
import { createBodyFromValue, getStatusInfo } from '@/ui/fetcher';
|
||||
import { getDefaultValue, getDefaultValues } from '@/ui/shared';
|
||||
import { InputField, ObjectInput } from '@/ui/inputs';
|
||||
import type { APIPlaygroundProps } from '@/render/playground';
|
||||
import { CodeBlock } from '@/ui/components/codeblock';
|
||||
import { type DynamicField, SchemaContext } from './contexts/schema';
|
||||
|
||||
interface APIPlaygroundFormData {
|
||||
authorization?: string | undefined;
|
||||
path?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
header?: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function APIPlayground({
|
||||
route,
|
||||
method = 'GET',
|
||||
authorization,
|
||||
path = [],
|
||||
header = [],
|
||||
query = [],
|
||||
body,
|
||||
schemas,
|
||||
}: APIPlaygroundProps & HTMLAttributes<HTMLFormElement>): React.ReactElement {
|
||||
const { baseUrl } = useApiContext();
|
||||
const dynamicRef = useRef(new Map<string, DynamicField>());
|
||||
const [input, setInput] = useState<APIPlaygroundFormData>();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
authorization: authorization?.defaultValue,
|
||||
path: getDefaultValues(path, schemas),
|
||||
query: getDefaultValues(query, schemas),
|
||||
header: getDefaultValues(header, schemas),
|
||||
body: body ? getDefaultValue(body, schemas) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const testQuery = useSWRImmutable(
|
||||
input ? [baseUrl, route, method, input] : null,
|
||||
async () => {
|
||||
if (!input) return;
|
||||
|
||||
let pathname = route;
|
||||
Object.keys(input.path ?? {}).forEach((key) => {
|
||||
const paramValue = input.path?.[key];
|
||||
|
||||
if (paramValue) pathname = pathname.replace(`{${key}}`, paramValue);
|
||||
});
|
||||
|
||||
const url = new URL(pathname, baseUrl ?? window.location.origin);
|
||||
Object.keys(input.query ?? {}).forEach((key) => {
|
||||
const paramValue = input.query?.[key];
|
||||
if (paramValue) url.searchParams.append(key, paramValue);
|
||||
});
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (input.authorization) {
|
||||
headers.append('Authorization', input.authorization);
|
||||
}
|
||||
|
||||
Object.keys(input.header ?? {}).forEach((key) => {
|
||||
const paramValue = input.header?.[key];
|
||||
|
||||
if (paramValue) headers.append(key, paramValue);
|
||||
});
|
||||
|
||||
const bodyValue =
|
||||
body && input.body && Object.keys(input.body).length > 0
|
||||
? createBodyFromValue(input.body, body, schemas, dynamicRef.current)
|
||||
: undefined;
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: bodyValue ? JSON.stringify(bodyValue) : undefined,
|
||||
});
|
||||
|
||||
const data: unknown = await response.json().catch(() => undefined);
|
||||
|
||||
return { status: response.status, data };
|
||||
},
|
||||
{
|
||||
shouldRetryOnError: false,
|
||||
},
|
||||
);
|
||||
|
||||
const statusInfo = testQuery.data
|
||||
? getStatusInfo(testQuery.data.status)
|
||||
: undefined;
|
||||
|
||||
const onSubmit = form.handleSubmit((value) => {
|
||||
setInput(value as APIPlaygroundFormData);
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<SchemaContext.Provider
|
||||
value={useMemo(
|
||||
() => ({ references: schemas, dynamic: dynamicRef }),
|
||||
[schemas],
|
||||
)}
|
||||
>
|
||||
<form
|
||||
className="not-prose flex flex-col gap-4 rounded-lg border bg-card p-4"
|
||||
onSubmit={onSubmit as React.FormEventHandler}
|
||||
>
|
||||
<div className="flex flex-row gap-2">
|
||||
<code className="flex-1 overflow-auto rounded-lg border bg-secondary px-3 py-1.5 text-sm">
|
||||
{route}
|
||||
</code>
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(buttonVariants({ color: 'secondary' }))}
|
||||
disabled={testQuery.isLoading}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Accordions type="multiple" className="-m-4 mt-2 border-0 text-sm">
|
||||
{authorization ? (
|
||||
<Accordion title="Authorization">
|
||||
<InputField
|
||||
name="Authorization"
|
||||
fieldName="authorization"
|
||||
field={authorization}
|
||||
/>
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
{path.length > 0 ? (
|
||||
<Accordion title="Path">
|
||||
{path.map((field) => (
|
||||
<InputField
|
||||
key={field.name}
|
||||
field={field}
|
||||
name={field.name}
|
||||
fieldName={`path.${field.name}`}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
{query.length > 0 ? (
|
||||
<Accordion title="Query">
|
||||
<div className="flex flex-col gap-2">
|
||||
{query.map((field) => (
|
||||
<InputField
|
||||
key={field.name}
|
||||
field={field}
|
||||
name={field.name}
|
||||
fieldName={`query.${field.name}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
{header.length > 0 ? (
|
||||
<Accordion title="Headers">
|
||||
{header.map((field) => (
|
||||
<InputField
|
||||
key={field.name}
|
||||
field={field}
|
||||
name={field.name}
|
||||
fieldName={`header.${field.name}`}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
{body ? (
|
||||
<Accordion title="Body">
|
||||
{body.type === 'object' ? (
|
||||
<ObjectInput field={body} fieldName="body" />
|
||||
) : (
|
||||
<InputField name="Body" field={body} fieldName="body" />
|
||||
)}
|
||||
</Accordion>
|
||||
) : null}
|
||||
</Accordions>
|
||||
|
||||
{testQuery.data && statusInfo ? (
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-card p-4">
|
||||
<div className="inline-flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<statusInfo.icon className={cn('size-4', statusInfo.color)} />
|
||||
{statusInfo.description}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{testQuery.data.status}
|
||||
</p>
|
||||
{testQuery.data.data ? (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(testQuery.data.data, null, 2)}
|
||||
className="max-h-[288px]"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</SchemaContext.Provider>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import {
|
||||
type PrimitiveRequestField,
|
||||
type ReferenceSchema,
|
||||
type RequestSchema,
|
||||
} from '@/render/playground';
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'rounded border px-1.5 py-1 text-xs font-medium leading-[12px]',
|
||||
{
|
||||
variants: {
|
||||
color: {
|
||||
green:
|
||||
'border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400',
|
||||
yellow:
|
||||
'border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
|
||||
red: 'border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400',
|
||||
blue: 'border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400',
|
||||
orange:
|
||||
'border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function getBadgeColor(
|
||||
method: string,
|
||||
): VariantProps<typeof badgeVariants>['color'] {
|
||||
switch (method) {
|
||||
case 'PUT':
|
||||
return 'yellow';
|
||||
case 'PATCH':
|
||||
return 'orange';
|
||||
case 'POST':
|
||||
return 'blue';
|
||||
case 'DELETE':
|
||||
return 'red';
|
||||
default:
|
||||
return 'green';
|
||||
}
|
||||
}
|
||||
|
||||
export type References = Record<string, RequestSchema>;
|
||||
|
||||
export function getDefaultValue(
|
||||
item: RequestSchema,
|
||||
references: References,
|
||||
): unknown {
|
||||
if (item.type === 'object')
|
||||
return Object.fromEntries(
|
||||
Object.entries(item.properties).map(([key, prop]) => [
|
||||
key,
|
||||
getDefaultValue(references[prop.schema], references),
|
||||
]),
|
||||
);
|
||||
|
||||
if (item.type === 'array') return [];
|
||||
if (item.type === 'null') return null;
|
||||
if (item.type === 'switcher')
|
||||
return getDefaultValue(
|
||||
resolve(Object.values(item.items)[0], references),
|
||||
references,
|
||||
);
|
||||
|
||||
return String(item.defaultValue);
|
||||
}
|
||||
|
||||
export function getDefaultValues(
|
||||
field: PrimitiveRequestField[],
|
||||
context: References,
|
||||
): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
field.map((p) => [p.name, getDefaultValue(p, context)]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve reference
|
||||
*/
|
||||
export function resolve(
|
||||
schema: RequestSchema | ReferenceSchema | string,
|
||||
references: References,
|
||||
): RequestSchema {
|
||||
if (typeof schema === 'string') return references[schema];
|
||||
if (schema.type !== 'ref') return schema;
|
||||
|
||||
return {
|
||||
...references[schema.schema],
|
||||
description: schema.description,
|
||||
isRequired: schema.isRequired,
|
||||
};
|
||||
}
|
||||
+4
-6
@@ -2,10 +2,10 @@ import { dump } from 'js-yaml';
|
||||
import { defaultRenderer } from '@/render/renderer';
|
||||
import { type GenerateOptions } from '@/generate';
|
||||
|
||||
export function renderPage(
|
||||
export function generateDocument(
|
||||
title: string,
|
||||
description: string | undefined,
|
||||
content: string[],
|
||||
content: string,
|
||||
options: GenerateOptions,
|
||||
): string {
|
||||
const banner = dump({
|
||||
@@ -19,7 +19,7 @@ export function renderPage(
|
||||
options.imports ?? [
|
||||
{
|
||||
names: Object.keys(defaultRenderer),
|
||||
from: 'fumadocs-ui/components/api',
|
||||
from: 'fumadocs-openapi/ui',
|
||||
},
|
||||
]
|
||||
)
|
||||
@@ -29,7 +29,5 @@ export function renderPage(
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const Root = options.renderer?.Root ?? defaultRenderer.Root;
|
||||
|
||||
return `---\n${banner}\n---\n\n${finalImports}\n\n${Root(content)}`;
|
||||
return `---\n${banner}\n---\n\n${finalImports}\n\n${content}`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import { noRef } from '@/utils/schema';
|
||||
|
||||
type Security = OpenAPI.SecuritySchemeObject & {
|
||||
scopes: string[];
|
||||
};
|
||||
|
||||
export function getScheme(
|
||||
requirement: OpenAPI.SecurityRequirementObject,
|
||||
document: OpenAPI.Document,
|
||||
): Security[] {
|
||||
const results: Security[] = [];
|
||||
const schemas = document.components?.securitySchemes ?? {};
|
||||
|
||||
for (const [key, scopes] of Object.entries(requirement)) {
|
||||
if (!(key in schemas)) return [];
|
||||
const schema = noRef(schemas[key]);
|
||||
|
||||
results.push({
|
||||
...schema,
|
||||
scopes,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function idToTitle(id: string): string {
|
||||
const result = [];
|
||||
for (const c of id) {
|
||||
if (result.length === 0) result.push(c.toLocaleUpperCase());
|
||||
else if (/^[A-Z]$/.test(c) && result.at(-1) !== ' ') result.push(' ', c);
|
||||
else if (c === '-') result.push(' ');
|
||||
else result.push(c);
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/ui/**/*.{ts,tsx}'],
|
||||
presets: [createPreset()],
|
||||
};
|
||||
+62
-1731
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,17 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { generate, generateFiles, generateTags } from '../src';
|
||||
|
||||
describe('Utilities', () => {
|
||||
test('Operation ID to Title', () => {
|
||||
expect(idToTitle('getKey')).toBe('Get Key');
|
||||
expect(idToTitle('requestId30')).toBe('Request Id30');
|
||||
expect(idToTitle('requestId-30')).toBe('Request Id 30');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generate documents', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
@@ -4,9 +4,9 @@ description: Special events hosted by the Museum
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.fake-museum-example.com/v1"}>
|
||||
|
||||
<API>
|
||||
|
||||
@@ -74,6 +74,10 @@ The number of days per page.
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/special-events"} path={[]} query={[{"name":"startDate","type":"string","defaultValue":"2023-02-23","isRequired":false},{"name":"endDate","type":"string","defaultValue":"2023-04-18","isRequired":false},{"name":"page","type":"number","defaultValue":2,"isRequired":false},{"name":"limit","type":"number","defaultValue":15,"isRequired":false}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -237,6 +241,10 @@ Price of a ticket for the special event
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/special-events"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"description":"Request payload for creating new special events at the museum.","properties":{"name":{"type":"ref","isRequired":true,"schema":"0"},"location":{"type":"ref","isRequired":true,"schema":"1"},"eventDescription":{"type":"ref","isRequired":true,"schema":"2"},"dates":{"type":"ref","isRequired":true,"schema":"3"},"price":{"type":"ref","isRequired":true,"schema":"5"}}}} schemas={{"0":{"type":"string","defaultValue":"Pirate Coding Workshop","isRequired":false,"description":"Name of the special event"},"1":{"type":"string","defaultValue":"Computer Room","isRequired":false,"description":"Location where the special event is held"},"2":{"type":"string","defaultValue":"Captain Blackbeard shares his love of the C...language. And possibly Arrrrr (R lang).","isRequired":false,"description":"Description of the special event"},"3":{"type":"array","description":"List of planned dates for the special event","isRequired":false,"items":"4"},"4":{"type":"string","defaultValue":"2023-10-29","isRequired":false},"5":{"type":"number","defaultValue":25,"isRequired":false,"description":"Price of a ticket for the special event"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -377,6 +385,10 @@ An identifier for a special event.
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/special-events/{eventId}"} path={[{"name":"eventId","type":"string","defaultValue":"dad4bce8-f5cb-4078-a211-995864315e39","isRequired":true}]} query={[]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -550,6 +562,10 @@ An identifier for a special event.
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"PATCH"} route={"/special-events/{eventId}"} path={[{"name":"eventId","type":"string","defaultValue":"dad4bce8-f5cb-4078-a211-995864315e39","isRequired":true}]} query={[]} header={[]} body={{"type":"object","isRequired":true,"description":"Request payload for updating an existing special event. Only included fields are updated in the event.","properties":{"name":{"type":"ref","isRequired":false,"schema":"0"},"location":{"type":"ref","isRequired":false,"schema":"1"},"eventDescription":{"type":"ref","isRequired":false,"schema":"2"},"dates":{"type":"ref","isRequired":false,"schema":"3"},"price":{"type":"ref","isRequired":false,"schema":"5"}}}} schemas={{"0":{"type":"string","defaultValue":"Pirate Coding Workshop","isRequired":false,"description":"Name of the special event"},"1":{"type":"string","defaultValue":"Computer Room","isRequired":false,"description":"Location where the special event is held"},"2":{"type":"string","defaultValue":"Captain Blackbeard shares his love of the C...language. And possibly Arrrrr (R lang).","isRequired":false,"description":"Description of the special event"},"3":{"type":"array","description":"List of planned dates for the special event","isRequired":false,"items":"4"},"4":{"type":"string","defaultValue":"2023-10-29","isRequired":false},"5":{"type":"number","defaultValue":25,"isRequired":false,"description":"Price of a ticket for the special event"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -691,6 +707,10 @@ An identifier for a special event.
|
||||
| `401` | Unauthorized |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"DELETE"} route={"/special-events/{eventId}"} path={[{"name":"eventId","type":"string","defaultValue":"dad4bce8-f5cb-4078-a211-995864315e39","isRequired":true}]} query={[]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -4,9 +4,9 @@ description: Operational information about the museum.
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.fake-museum-example.com/v1"}>
|
||||
|
||||
<API>
|
||||
|
||||
@@ -64,6 +64,10 @@ The number of days per page.
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/museum-hours"} path={[]} query={[{"name":"startDate","type":"string","defaultValue":"2023-02-23","isRequired":false},{"name":"page","type":"number","defaultValue":2,"isRequired":false},{"name":"limit","type":"number","defaultValue":15,"isRequired":false}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -4,9 +4,9 @@ description: Museum tickets for general entrance or special events.
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.fake-museum-example.com/v1"}>
|
||||
|
||||
<API>
|
||||
|
||||
@@ -80,6 +80,10 @@ Phone number for the ticket purchaser (optional).
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/tickets"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"description":"Request payload used for purchasing museum tickets.","properties":{"ticketType":{"type":"ref","isRequired":true,"schema":"0"},"eventId":{"type":"ref","isRequired":false,"schema":"1"},"ticketDate":{"type":"ref","isRequired":true,"schema":"2"},"email":{"type":"ref","isRequired":true,"schema":"3"},"phone":{"type":"ref","isRequired":false,"schema":"4"}}}} schemas={{"0":{"type":"string","defaultValue":"event","isRequired":false,"description":"Type of ticket being purchased. Use `general` for regular museum entry and `event` for tickets to special events."},"1":{"type":"string","defaultValue":"3be6453c-03eb-4357-ae5a-984a0e574a54","isRequired":false,"description":"Identifier for a special event."},"2":{"type":"string","defaultValue":"2023-10-29","isRequired":false,"description":"Date that the ticket is valid for."},"3":{"type":"string","defaultValue":"[email protected]","isRequired":false,"description":"Email address for ticket purchaser."},"4":{"type":"string","defaultValue":"+1(234)-567-8910","isRequired":false,"description":"Phone number for the ticket purchaser (optional)."}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -216,6 +220,10 @@ An identifier for a ticket to a museum event. Used to generate ticket image.
|
||||
| `400` | Bad request |
|
||||
| `404` | Not found |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Basic","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/tickets/{ticketId}/qr"} path={[{"name":"ticketId","type":"string","defaultValue":"a54a57ca-36f8-421b-a6b4-2e8f26858a4c","isRequired":true}]} query={[]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -3,9 +3,9 @@ title: Swagger Petstore
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"http://petstore.swagger.io/v1"}>
|
||||
|
||||
<API>
|
||||
|
||||
@@ -30,6 +30,10 @@ How many items to return at one time (max 100)
|
||||
| `200` | A paged array of pets |
|
||||
| `default` | unexpected error |
|
||||
|
||||
<APIPlayground authorization={undefined} method={"GET"} route={"/pets"} path={[]} query={[{"name":"limit","type":"number","defaultValue":"","isRequired":false}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -158,6 +162,10 @@ export interface Response {
|
||||
| `201` | Null response |
|
||||
| `default` | unexpected error |
|
||||
|
||||
<APIPlayground authorization={undefined} method={"POST"} route={"/pets"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"id":{"type":"ref","isRequired":true,"schema":"0"},"name":{"type":"ref","isRequired":true,"schema":"1"},"tag":{"type":"ref","isRequired":false,"schema":"2"}}}} schemas={{"0":{"type":"number","defaultValue":"","isRequired":false},"1":{"type":"string","defaultValue":"","isRequired":false},"2":{"type":"string","defaultValue":"","isRequired":false}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -246,6 +254,10 @@ The id of the pet to retrieve
|
||||
| `200` | Expected response to a valid request |
|
||||
| `default` | unexpected error |
|
||||
|
||||
<APIPlayground authorization={undefined} method={"GET"} route={"/pets/{petId}"} path={[{"name":"petId","type":"string","defaultValue":"","isRequired":true}]} query={[]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
title: apis
|
||||
title: Apis
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.unkey.dev"}>
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"GET"} route={"/v1/apis.getApi"}>
|
||||
|
||||
## getApi
|
||||
## Get Api
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -44,6 +44,10 @@ The id of the api to fetch
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/v1/apis.getApi"} path={[]} query={[{"name":"apiId","type":"string","defaultValue":"api_1234","isRequired":true,"description":"The id of the api to fetch"}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -471,9 +475,468 @@ export interface Response {
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/apis.createApi"}>
|
||||
|
||||
## Create Api
|
||||
|
||||
### Authorization
|
||||
|
||||
<Property name={"Authorization"} type={"Bearer <token>"} required={true}>
|
||||
|
||||
In: `header`
|
||||
|
||||
</Property>
|
||||
|
||||
### Request Body
|
||||
|
||||
<Property name={"name"} type={"string"} required={true} deprecated={undefined}>
|
||||
|
||||
The name for your API. This is not customer facing.
|
||||
|
||||
<span>Example: `"my-api"`</span>
|
||||
|
||||
<span>Minimum length: `3`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The configuration for an api |
|
||||
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
|
||||
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
|
||||
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
|
||||
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
|
||||
| `409` | This response is sent when a request conflicts with the current state of the server. |
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/apis.createApi"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"name":{"type":"ref","isRequired":true,"schema":"0"}}}} schemas={{"0":{"type":"string","defaultValue":"my-api","isRequired":false,"description":"The name for your API. This is not customer facing."}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
<Requests items={["cURL","JavaScript"]}>
|
||||
|
||||
<Request value={"cURL"}>
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.unkey.dev/v1/apis.createApi" \
|
||||
-d '{
|
||||
"name": "my-api"
|
||||
}'
|
||||
```
|
||||
|
||||
</Request>
|
||||
|
||||
<Request value={"JavaScript"}>
|
||||
|
||||
```js
|
||||
fetch("https://api.unkey.dev/v1/apis.createApi", {
|
||||
method: "POST"
|
||||
});
|
||||
```
|
||||
|
||||
</Request>
|
||||
|
||||
</Requests>
|
||||
|
||||
<Responses items={["200","400","401","403","404","409","429","500"]}>
|
||||
|
||||
<Response value={"200"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"apiId": "api_134"
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
/**
|
||||
* The id of the api
|
||||
*/
|
||||
apiId: string;
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"400"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "BAD_REQUEST";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"401"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/UNAUTHORIZED",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "UNAUTHORIZED";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"403"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "FORBIDDEN",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/FORBIDDEN",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "FORBIDDEN";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"404"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/NOT_FOUND",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "NOT_FOUND";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"409"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "CONFLICT",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/CONFLICT",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "CONFLICT";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"429"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "TOO_MANY_REQUESTS",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/TOO_MANY_REQUESTS",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "TOO_MANY_REQUESTS";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
<Response value={"500"}>
|
||||
|
||||
<ResponseTypes>
|
||||
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"docs": "https://unkey.dev/docs/api-reference/errors/code/INTERNAL_SERVER_ERROR",
|
||||
"message": "string",
|
||||
"requestId": "req_1234"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
error: {
|
||||
/**
|
||||
* A machine readable error code.
|
||||
*/
|
||||
code: "INTERNAL_SERVER_ERROR";
|
||||
/**
|
||||
* A link to our documentation with more details about this error code
|
||||
*/
|
||||
docs: string;
|
||||
/**
|
||||
* A human readable explanation of what went wrong
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Please always include the requestId in your error report
|
||||
*/
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
</ResponseTypes>
|
||||
|
||||
</Response>
|
||||
|
||||
</Responses>
|
||||
|
||||
</APIExample>
|
||||
|
||||
</API>
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"GET"} route={"/v1/apis.listKeys"}>
|
||||
|
||||
## listKeys
|
||||
## List Keys
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -523,12 +986,6 @@ If provided, this will only return keys where the `ownerId` matches.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"decrypt"} type={"boolean | null"} required={false} deprecated={false}>
|
||||
|
||||
Decrypt and display the raw key. Only possible if the key was encrypted when generated.
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The configuration for an api |
|
||||
@@ -540,6 +997,10 @@ Decrypt and display the raw key. Only possible if the key was encrypted when gen
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/v1/apis.listKeys"} path={[]} query={[{"name":"apiId","type":"string","defaultValue":"api_1234","isRequired":true,"description":"The id of the api to fetch"},{"name":"limit","type":"number","defaultValue":100,"isRequired":false,"description":"The maximum number of keys to return"},{"name":"cursor","type":"string","defaultValue":"","isRequired":false,"description":"Use this to fetch the next page of results. A new cursor will be returned in the response if there are more results."},{"name":"ownerId","type":"string","defaultValue":"","isRequired":false,"description":"If provided, this will only return keys where the `ownerId` matches."}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -549,7 +1010,7 @@ Decrypt and display the raw key. Only possible if the key was encrypted when gen
|
||||
<Request value={"cURL"}>
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=string&ownerId=string&decrypt=true"
|
||||
curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=string&ownerId=string"
|
||||
```
|
||||
|
||||
</Request>
|
||||
@@ -557,7 +1018,7 @@ curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cur
|
||||
<Request value={"JavaScript"}>
|
||||
|
||||
```js
|
||||
fetch("https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=string&ownerId=string&decrypt=true", {
|
||||
fetch("https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=string&ownerId=string", {
|
||||
method: "GET"
|
||||
});
|
||||
```
|
||||
@@ -592,7 +1053,6 @@ fetch("https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=st
|
||||
"stripeCustomerId": "cus_1234"
|
||||
},
|
||||
"createdAt": 0,
|
||||
"updatedAt": 0,
|
||||
"deletedAt": 0,
|
||||
"expires": 0,
|
||||
"remaining": 1000,
|
||||
@@ -601,20 +1061,16 @@ fetch("https://api.unkey.dev/v1/apis.listKeys?apiId=api_1234&limit=100&cursor=st
|
||||
"amount": 10
|
||||
},
|
||||
"ratelimit": {
|
||||
"async": true,
|
||||
"type": "fast",
|
||||
"limit": 10,
|
||||
"duration": 60
|
||||
"refillRate": 1,
|
||||
"refillInterval": 60
|
||||
},
|
||||
"roles": [
|
||||
"admin",
|
||||
"finance"
|
||||
],
|
||||
"permissions": [
|
||||
"domain.dns.create_record",
|
||||
"finance.read_receipt"
|
||||
],
|
||||
"enabled": true,
|
||||
"plaintext": "string"
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"cursor": "eyJrZXkiOiJrZXlfMTIzNCJ9",
|
||||
@@ -664,11 +1120,7 @@ export interface Response {
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was created
|
||||
*/
|
||||
createdAt: number;
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was last updated
|
||||
*/
|
||||
updatedAt?: number;
|
||||
createdAt?: number;
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was deleted. We don't delete the key outright, you can restore it later.
|
||||
*/
|
||||
@@ -702,11 +1154,10 @@ export interface Response {
|
||||
* Unkey comes with per-key ratelimiting out of the box.
|
||||
*/
|
||||
ratelimit?: {
|
||||
async: boolean;
|
||||
/**
|
||||
* Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate.
|
||||
*/
|
||||
type?: "fast" | "consistent";
|
||||
type: "fast" | "consistent";
|
||||
/**
|
||||
* The total amount of burstable requests.
|
||||
*/
|
||||
@@ -714,32 +1165,20 @@ export interface Response {
|
||||
/**
|
||||
* How many tokens to refill during each refillInterval.
|
||||
*/
|
||||
refillRate?: number;
|
||||
refillRate: number;
|
||||
/**
|
||||
* Determines the speed at which tokens are refilled, in milliseconds.
|
||||
*/
|
||||
refillInterval?: number;
|
||||
/**
|
||||
* The duration of the ratelimit window, in milliseconds.
|
||||
*/
|
||||
duration: number;
|
||||
refillInterval: number;
|
||||
};
|
||||
/**
|
||||
* All roles this key belongs to
|
||||
*/
|
||||
roles?: string[];
|
||||
/**
|
||||
* All permissions this key has
|
||||
*/
|
||||
permissions?: string[];
|
||||
/**
|
||||
* Sets if key is enabled or disabled. Disabled keys are not valid.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* The key in plaintext
|
||||
*/
|
||||
plaintext?: string;
|
||||
}[];
|
||||
/**
|
||||
* The cursor to use for the next page of results, if no cursor is returned, there are no more results
|
||||
@@ -1116,9 +1555,9 @@ export interface Response {
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/apis.deleteKeys"}>
|
||||
<APIInfo method={"POST"} route={"/v1/apis.deleteApi"}>
|
||||
|
||||
## deleteKeys
|
||||
## Delete Api
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -1132,7 +1571,7 @@ In: `header`
|
||||
|
||||
<Property name={"apiId"} type={"string"} required={true} deprecated={undefined}>
|
||||
|
||||
The id of the api, that the keys belong to.
|
||||
The id of the api to delete
|
||||
|
||||
<span>Example: `"api_1234"`</span>
|
||||
|
||||
@@ -1140,17 +1579,9 @@ The id of the api, that the keys belong to.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"permanent"} type={"boolean"} required={false} deprecated={undefined}>
|
||||
|
||||
If true, the keys will be permanently deleted. If false, the keys will be soft-deleted and can be restored later.
|
||||
|
||||
<span>Default: `false`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The keys have been deleted |
|
||||
| `200` | The api was successfully deleted, it may take up to 30s for this to take effect in all regions |
|
||||
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
|
||||
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
|
||||
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
|
||||
@@ -1159,6 +1590,10 @@ If true, the keys will be permanently deleted. If false, the keys will be soft-d
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/apis.deleteApi"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"apiId":{"type":"ref","isRequired":true,"schema":"0"}}}} schemas={{"0":{"type":"string","defaultValue":"api_1234","isRequired":false,"description":"The id of the api to delete"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -1168,10 +1603,9 @@ If true, the keys will be permanently deleted. If false, the keys will be soft-d
|
||||
<Request value={"cURL"}>
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.unkey.dev/v1/apis.deleteKeys" \
|
||||
curl -X POST "https://api.unkey.dev/v1/apis.deleteApi" \
|
||||
-d '{
|
||||
"apiId": "api_1234",
|
||||
"permanent": false
|
||||
"apiId": "api_1234"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -1180,7 +1614,7 @@ curl -X POST "https://api.unkey.dev/v1/apis.deleteKeys" \
|
||||
<Request value={"JavaScript"}>
|
||||
|
||||
```js
|
||||
fetch("https://api.unkey.dev/v1/apis.deleteKeys", {
|
||||
fetch("https://api.unkey.dev/v1/apis.deleteApi", {
|
||||
method: "POST"
|
||||
});
|
||||
```
|
||||
@@ -1198,9 +1632,7 @@ fetch("https://api.unkey.dev/v1/apis.deleteKeys", {
|
||||
<ExampleResponse>
|
||||
|
||||
```json
|
||||
{
|
||||
"deletedKeys": 0
|
||||
}
|
||||
{}
|
||||
```
|
||||
|
||||
</ExampleResponse>
|
||||
@@ -1208,12 +1640,7 @@ fetch("https://api.unkey.dev/v1/apis.deleteKeys", {
|
||||
<TypeScriptResponse>
|
||||
|
||||
```ts
|
||||
export interface Response {
|
||||
/**
|
||||
* The number of keys that were deleted
|
||||
*/
|
||||
deletedKeys: number;
|
||||
}
|
||||
export interface Response {}
|
||||
```
|
||||
|
||||
</TypeScriptResponse>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
title: keys
|
||||
title: Keys
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.unkey.dev"}>
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"GET"} route={"/v1/keys.getKey"}>
|
||||
|
||||
## getKey
|
||||
## Get Key
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -33,12 +33,6 @@ The id of the key to fetch
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"decrypt"} type={"boolean | null"} required={false} deprecated={false}>
|
||||
|
||||
Decrypt and display the raw key. Only possible if the key was encrypted when generated.
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The configuration for a single key |
|
||||
@@ -50,6 +44,10 @@ Decrypt and display the raw key. Only possible if the key was encrypted when gen
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/v1/keys.getKey"} path={[]} query={[{"name":"keyId","type":"string","defaultValue":"key_1234","isRequired":true,"description":"The id of the key to fetch"}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -59,7 +57,7 @@ Decrypt and display the raw key. Only possible if the key was encrypted when gen
|
||||
<Request value={"cURL"}>
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_1234&decrypt=true"
|
||||
curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_1234"
|
||||
```
|
||||
|
||||
</Request>
|
||||
@@ -67,7 +65,7 @@ curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_1234&decrypt=true"
|
||||
<Request value={"JavaScript"}>
|
||||
|
||||
```js
|
||||
fetch("https://api.unkey.dev/v1/keys.getKey?keyId=key_1234&decrypt=true", {
|
||||
fetch("https://api.unkey.dev/v1/keys.getKey?keyId=key_1234", {
|
||||
method: "GET"
|
||||
});
|
||||
```
|
||||
@@ -100,7 +98,6 @@ fetch("https://api.unkey.dev/v1/keys.getKey?keyId=key_1234&decrypt=true", {
|
||||
"stripeCustomerId": "cus_1234"
|
||||
},
|
||||
"createdAt": 0,
|
||||
"updatedAt": 0,
|
||||
"deletedAt": 0,
|
||||
"expires": 0,
|
||||
"remaining": 1000,
|
||||
@@ -109,20 +106,16 @@ fetch("https://api.unkey.dev/v1/keys.getKey?keyId=key_1234&decrypt=true", {
|
||||
"amount": 10
|
||||
},
|
||||
"ratelimit": {
|
||||
"async": true,
|
||||
"type": "fast",
|
||||
"limit": 10,
|
||||
"duration": 60
|
||||
"refillRate": 1,
|
||||
"refillInterval": 60
|
||||
},
|
||||
"roles": [
|
||||
"admin",
|
||||
"finance"
|
||||
],
|
||||
"permissions": [
|
||||
"domain.dns.create_record",
|
||||
"finance.read_receipt"
|
||||
],
|
||||
"enabled": true,
|
||||
"plaintext": "string"
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -167,11 +160,7 @@ export interface Response {
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was created
|
||||
*/
|
||||
createdAt: number;
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was last updated
|
||||
*/
|
||||
updatedAt?: number;
|
||||
createdAt?: number;
|
||||
/**
|
||||
* The unix timestamp in milliseconds when the key was deleted. We don't delete the key outright, you can restore it later.
|
||||
*/
|
||||
@@ -205,11 +194,10 @@ export interface Response {
|
||||
* Unkey comes with per-key ratelimiting out of the box.
|
||||
*/
|
||||
ratelimit?: {
|
||||
async: boolean;
|
||||
/**
|
||||
* Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate.
|
||||
*/
|
||||
type?: "fast" | "consistent";
|
||||
type: "fast" | "consistent";
|
||||
/**
|
||||
* The total amount of burstable requests.
|
||||
*/
|
||||
@@ -217,32 +205,20 @@ export interface Response {
|
||||
/**
|
||||
* How many tokens to refill during each refillInterval.
|
||||
*/
|
||||
refillRate?: number;
|
||||
refillRate: number;
|
||||
/**
|
||||
* Determines the speed at which tokens are refilled, in milliseconds.
|
||||
*/
|
||||
refillInterval?: number;
|
||||
/**
|
||||
* The duration of the ratelimit window, in milliseconds.
|
||||
*/
|
||||
duration: number;
|
||||
refillInterval: number;
|
||||
};
|
||||
/**
|
||||
* All roles this key belongs to
|
||||
*/
|
||||
roles?: string[];
|
||||
/**
|
||||
* All permissions this key has
|
||||
*/
|
||||
permissions?: string[];
|
||||
/**
|
||||
* Sets if key is enabled or disabled. Disabled keys are not valid.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* The key in plaintext
|
||||
*/
|
||||
plaintext?: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -612,7 +588,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/keys.deleteKey"}>
|
||||
|
||||
## deleteKey
|
||||
## Delete Key
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -645,6 +621,10 @@ The id of the key to revoke
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/keys.deleteKey"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"keyId":{"type":"ref","isRequired":true,"schema":"0"}}}} schemas={{"0":{"type":"string","defaultValue":"key_1234","isRequired":false,"description":"The id of the key to revoke"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -1060,7 +1040,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/keys.createKey"}>
|
||||
|
||||
## createKey
|
||||
## Create Key
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -1089,7 +1069,7 @@ For example Stripe famously prefixes their customer ids with cus_ or their api k
|
||||
The underscore is automatically added if you are defining a prefix, for example: "prefix": "abc" will result in a key like abc_xxxxxxxxx
|
||||
|
||||
|
||||
<span>Maximum length: `16`</span>
|
||||
<span>Maximum length: `8`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
@@ -1140,20 +1120,12 @@ This is a place for dynamic meta data, anything that feels useful for you should
|
||||
|
||||
<Property name={"roles"} type={"array<string>"} required={false} deprecated={undefined}>
|
||||
|
||||
A list of roles that this key should have. If the role does not exist, an error is thrown
|
||||
A list of roles that this key should have. New roles will be created if they don't exist yet.
|
||||
|
||||
<span>Example: `["admin","finance"]`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"permissions"} type={"array<string>"} required={false} deprecated={undefined}>
|
||||
|
||||
A list of permissions that this key should have. If the permission does not exist, an error is thrown
|
||||
|
||||
<span>Example: `["domains.create_record","say_hello"]`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"expires"} type={"integer"} required={false} deprecated={undefined}>
|
||||
|
||||
You can auto expire keys by providing a unix timestamp in milliseconds. Once Keys expire they will automatically be disabled and are no longer valid unless you enable them again.
|
||||
@@ -1200,23 +1172,15 @@ The number of verifications to refill for each occurrence is determined individu
|
||||
|
||||
<Property name={"ratelimit"} type={"object"} required={false} deprecated={undefined}>
|
||||
|
||||
Unkey comes with per-key fixed-window ratelimiting out of the box.
|
||||
Unkey comes with per-key ratelimiting out of the box.
|
||||
|
||||
<span>Example: `{"type":"fast","limit":10,"duration":60000}`</span>
|
||||
<span>Example: `{"type":"fast","limit":10,"refillRate":1,"refillInterval":60}`</span>
|
||||
|
||||
<ObjectCollapsible name={"ratelimit"}>
|
||||
|
||||
<Property name={"async"} type={"boolean"} required={false} deprecated={undefined}>
|
||||
<Property name={"type"} type={"string"} required={true} deprecated={undefined}>
|
||||
|
||||
Async will return a response immediately, lowering latency at the cost of accuracy. Will be required soon.
|
||||
|
||||
<span>Default: `true`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"type"} type={"string"} required={false} deprecated={true}>
|
||||
|
||||
Deprecated, used `async`. Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate.
|
||||
Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate.
|
||||
|
||||
<span>Default: `"fast"`</span>
|
||||
|
||||
@@ -1226,23 +1190,13 @@ Deprecated, used `async`. Fast ratelimiting doesn't add latency, while consisten
|
||||
|
||||
<Property name={"limit"} type={"integer"} required={true} deprecated={undefined}>
|
||||
|
||||
The total amount of requests in a given interval.
|
||||
The total amount of burstable requests.
|
||||
|
||||
<span>Minimum: `1`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"duration"} type={"integer"} required={false} deprecated={undefined}>
|
||||
|
||||
The window duration in milliseconds. Will be required soon.
|
||||
|
||||
<span>Example: `60000`</span>
|
||||
|
||||
<span>Minimum: `1000`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"refillRate"} type={"integer"} required={false} deprecated={true}>
|
||||
<Property name={"refillRate"} type={"integer"} required={true} deprecated={undefined}>
|
||||
|
||||
How many tokens to refill during each refillInterval.
|
||||
|
||||
@@ -1250,9 +1204,9 @@ How many tokens to refill during each refillInterval.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"refillInterval"} type={"integer"} required={false} deprecated={true}>
|
||||
<Property name={"refillInterval"} type={"integer"} required={true} deprecated={undefined}>
|
||||
|
||||
The refill timeframe, in milliseconds.
|
||||
Determines the speed at which tokens are refilled, in milliseconds.
|
||||
|
||||
<span>Minimum: `1`</span>
|
||||
|
||||
@@ -1272,22 +1226,6 @@ Sets if key is enabled or disabled. Disabled keys are not valid.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"environment"} type={"string"} required={false} deprecated={undefined}>
|
||||
|
||||
Environments allow you to divide your keyspace.
|
||||
|
||||
Some applications like Stripe, Clerk, WorkOS and others have a concept of "live" and "test" keys to
|
||||
give the developer a way to develop their own application without the risk of modifying real world
|
||||
resources.
|
||||
|
||||
When you set an environment, we will return it back to you when validating the key, so you can
|
||||
handle it correctly.
|
||||
|
||||
|
||||
<span>Maximum length: `256`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The configuration for an api |
|
||||
@@ -1299,6 +1237,10 @@ handle it correctly.
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/keys.createKey"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"apiId":{"type":"ref","isRequired":true,"schema":"0"},"prefix":{"type":"ref","isRequired":false,"schema":"1"},"name":{"type":"ref","isRequired":false,"schema":"2"},"byteLength":{"type":"ref","isRequired":false,"schema":"3"},"ownerId":{"type":"ref","isRequired":false,"schema":"4"},"meta":{"type":"ref","isRequired":false,"schema":"5"},"roles":{"type":"ref","isRequired":false,"schema":"6"},"expires":{"type":"ref","isRequired":false,"schema":"8"},"remaining":{"type":"ref","isRequired":false,"schema":"9"},"refill":{"type":"ref","isRequired":false,"schema":"10"},"ratelimit":{"type":"ref","isRequired":false,"schema":"13"},"enabled":{"type":"ref","isRequired":false,"schema":"18"}}}} schemas={{"0":{"type":"string","defaultValue":"api_123","isRequired":false,"description":"Choose an `API` where this key should be created."},"1":{"type":"string","defaultValue":"","isRequired":false,"description":"To make it easier for your users to understand which product an api key belongs to, you can add prefix them.\n\nFor example Stripe famously prefixes their customer ids with cus_ or their api keys with sk_live_.\n\nThe underscore is automatically added if you are defining a prefix, for example: \"prefix\": \"abc\" will result in a key like abc_xxxxxxxxx\n"},"2":{"type":"string","defaultValue":"my key","isRequired":false,"description":"The name for your Key. This is not customer facing."},"3":{"type":"number","defaultValue":"","isRequired":false,"description":"The byte length used to generate your key determines its entropy as well as its length. Higher is better, but keys become longer and more annoying to handle. The default is 16 bytes, or 2^^128 possible combinations."},"4":{"type":"string","defaultValue":"team_123","isRequired":false,"description":"Your user’s Id. This will provide a link between Unkey and your customer record.\nWhen validating a key, we will return this back to you, so you can clearly identify your user from their api key."},"5":{"type":"object","isRequired":false,"description":"This is a place for dynamic meta data, anything that feels useful for you should go here","properties":{},"additionalProperties":true},"6":{"type":"array","description":"A list of roles that this key should have. New roles will be created if they don't exist yet.","isRequired":false,"items":"7"},"7":{"type":"string","defaultValue":"","isRequired":false},"8":{"type":"number","defaultValue":1623869797161,"isRequired":false,"description":"You can auto expire keys by providing a unix timestamp in milliseconds. Once Keys expire they will automatically be disabled and are no longer valid unless you enable them again."},"9":{"type":"number","defaultValue":1000,"isRequired":false,"description":"You can limit the number of requests a key can make. Once a key reaches 0 remaining requests, it will automatically be disabled and is no longer valid unless you update it."},"10":{"type":"object","isRequired":false,"description":"Unkey enables you to refill verifications for each key at regular intervals.","properties":{"interval":{"type":"ref","isRequired":true,"schema":"11"},"amount":{"type":"ref","isRequired":true,"schema":"12"}}},"11":{"type":"string","defaultValue":"","isRequired":false,"description":"Unkey will automatically refill verifications at the set interval."},"12":{"type":"number","defaultValue":"","isRequired":false,"description":"The number of verifications to refill for each occurrence is determined individually for each key."},"13":{"type":"object","isRequired":false,"description":"Unkey comes with per-key ratelimiting out of the box.","properties":{"type":{"type":"ref","isRequired":true,"schema":"14"},"limit":{"type":"ref","isRequired":true,"schema":"15"},"refillRate":{"type":"ref","isRequired":true,"schema":"16"},"refillInterval":{"type":"ref","isRequired":true,"schema":"17"}}},"14":{"type":"string","defaultValue":"","isRequired":false,"description":"Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate."},"15":{"type":"number","defaultValue":"","isRequired":false,"description":"The total amount of burstable requests."},"16":{"type":"number","defaultValue":"","isRequired":false,"description":"How many tokens to refill during each refillInterval."},"17":{"type":"number","defaultValue":"","isRequired":false,"description":"Determines the speed at which tokens are refilled, in milliseconds."},"18":{"type":"boolean","defaultValue":false,"isRequired":false,"description":"Sets if key is enabled or disabled. Disabled keys are not valid."}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -1323,10 +1265,6 @@ curl -X POST "https://api.unkey.dev/v1/keys.createKey" \
|
||||
"admin",
|
||||
"finance"
|
||||
],
|
||||
"permissions": [
|
||||
"domains.create_record",
|
||||
"say_hello"
|
||||
],
|
||||
"expires": 1623869797161,
|
||||
"remaining": 1000,
|
||||
"refill": {
|
||||
@@ -1336,10 +1274,10 @@ curl -X POST "https://api.unkey.dev/v1/keys.createKey" \
|
||||
"ratelimit": {
|
||||
"type": "fast",
|
||||
"limit": 10,
|
||||
"duration": 60000
|
||||
"refillRate": 1,
|
||||
"refillInterval": 60
|
||||
},
|
||||
"enabled": false,
|
||||
"environment": "string"
|
||||
"enabled": false
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -1755,7 +1693,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/keys.verifyKey"}>
|
||||
|
||||
## verifyKey
|
||||
## Verify Key
|
||||
|
||||
### Request Body
|
||||
|
||||
@@ -1778,56 +1716,6 @@ The key to verify
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"authorization"} type={"object"} required={false} deprecated={undefined}>
|
||||
|
||||
Perform RBAC checks
|
||||
|
||||
<ObjectCollapsible name={"authorization"}>
|
||||
|
||||
<Property name={"permissions"} type={"LiteralClause | And | Or | null"} required={false} deprecated={undefined}>
|
||||
|
||||
A query for which permissions you require
|
||||
|
||||
<ObjectCollapsible name={"And"}>
|
||||
|
||||
<Property name={"and"} type={"array<LiteralClause | And | Or | null>"} required={true} deprecated={undefined}>
|
||||
|
||||
</Property>
|
||||
|
||||
</ObjectCollapsible>
|
||||
|
||||
<ObjectCollapsible name={"Or"}>
|
||||
|
||||
<Property name={"or"} type={"array<LiteralClause | And | Or | null>"} required={true} deprecated={undefined}>
|
||||
|
||||
</Property>
|
||||
|
||||
</ObjectCollapsible>
|
||||
|
||||
</Property>
|
||||
|
||||
</ObjectCollapsible>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"ratelimit"} type={"object"} required={false} deprecated={undefined}>
|
||||
|
||||
<ObjectCollapsible name={"ratelimit"}>
|
||||
|
||||
<Property name={"cost"} type={"integer"} required={false} deprecated={undefined}>
|
||||
|
||||
Override how many tokens are deducted during the ratelimit operation.
|
||||
|
||||
<span>Default: `1`</span>
|
||||
|
||||
<span>Minimum: `0`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
</ObjectCollapsible>
|
||||
|
||||
</Property>
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | The verification result |
|
||||
@@ -1839,6 +1727,10 @@ Override how many tokens are deducted during the ratelimit operation.
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={undefined} method={"POST"} route={"/v1/keys.verifyKey"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"apiId":{"type":"ref","isRequired":false,"schema":"0"},"key":{"type":"ref","isRequired":true,"schema":"1"}}}} schemas={{"0":{"type":"string","defaultValue":"api_1234","isRequired":false,"description":"The id of the api where the key belongs to. This is optional for now but will be required soon.\nThe key will be verified against the api's configuration. If the key does not belong to the api, the verification will fail."},"1":{"type":"string","defaultValue":"sk_1234","isRequired":false,"description":"The key to verify"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -1851,13 +1743,7 @@ Override how many tokens are deducted during the ratelimit operation.
|
||||
curl -X POST "https://api.unkey.dev/v1/keys.verifyKey" \
|
||||
-d '{
|
||||
"apiId": "api_1234",
|
||||
"key": "sk_1234",
|
||||
"authorization": {
|
||||
"permissions": "string"
|
||||
},
|
||||
"ratelimit": {
|
||||
"cost": 1
|
||||
}
|
||||
"key": "sk_1234"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -1903,13 +1789,8 @@ fetch("https://api.unkey.dev/v1/keys.verifyKey", {
|
||||
"reset": 3600000
|
||||
},
|
||||
"remaining": 1000,
|
||||
"code": "VALID",
|
||||
"enabled": true,
|
||||
"permissions": [
|
||||
"dns.record.update",
|
||||
"dns.record.delete"
|
||||
],
|
||||
"environment": "test"
|
||||
"code": "NOT_FOUND",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1970,9 +1851,8 @@ export interface Response {
|
||||
*/
|
||||
remaining?: number;
|
||||
/**
|
||||
* A machine readable code why the key is not valid.
|
||||
* If the key is invalid this field will be set to the reason why it is invalid.
|
||||
* Possible values are:
|
||||
* - VALID: the key is valid and you should proceed
|
||||
* - NOT_FOUND: the key does not exist or has expired
|
||||
* - FORBIDDEN: the key is not allowed to access the api
|
||||
* - USAGE_EXCEEDED: the key has exceeded its request limit
|
||||
@@ -1982,8 +1862,7 @@ export interface Response {
|
||||
* - INSUFFICIENT_PERMISSIONS: you do not have the required permissions to perform this action
|
||||
*
|
||||
*/
|
||||
code:
|
||||
| "VALID"
|
||||
code?:
|
||||
| "NOT_FOUND"
|
||||
| "FORBIDDEN"
|
||||
| "USAGE_EXCEEDED"
|
||||
@@ -1995,14 +1874,6 @@ export interface Response {
|
||||
* Sets the key to be enabled or disabled. Disabled keys will not verify.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* A list of all the permissions this key is connected to.
|
||||
*/
|
||||
permissions?: string[];
|
||||
/**
|
||||
* The environment of the key, this is what what you set when you crated the key
|
||||
*/
|
||||
environment?: string;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2372,7 +2243,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/keys.updateKey"}>
|
||||
|
||||
## updateKey
|
||||
## Update Key
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -2440,7 +2311,7 @@ Unkey comes with per-key ratelimiting out of the box. Set `null` to disable.
|
||||
|
||||
<ObjectCollapsible name={"ratelimit"}>
|
||||
|
||||
<Property name={"type"} type={"string"} required={false} deprecated={true}>
|
||||
<Property name={"type"} type={"string"} required={true} deprecated={undefined}>
|
||||
|
||||
Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate.
|
||||
|
||||
@@ -2448,14 +2319,6 @@ Fast ratelimiting doesn't add latency, while consistent ratelimiting is more acc
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"async"} type={"boolean"} required={false} deprecated={undefined}>
|
||||
|
||||
Asnyc ratelimiting doesn't add latency, while sync ratelimiting is more accurate.
|
||||
|
||||
<span>Default: `false`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"limit"} type={"integer"} required={true} deprecated={undefined}>
|
||||
|
||||
The total amount of burstable requests.
|
||||
@@ -2464,7 +2327,7 @@ The total amount of burstable requests.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"refillRate"} type={"integer"} required={false} deprecated={true}>
|
||||
<Property name={"refillRate"} type={"integer"} required={true} deprecated={undefined}>
|
||||
|
||||
How many tokens to refill during each refillInterval.
|
||||
|
||||
@@ -2480,14 +2343,6 @@ Determines the speed at which tokens are refilled, in milliseconds.
|
||||
|
||||
</Property>
|
||||
|
||||
<Property name={"duration"} type={"integer"} required={false} deprecated={undefined}>
|
||||
|
||||
The duration of each ratelimit window, in milliseconds.
|
||||
|
||||
<span>Minimum: `1`</span>
|
||||
|
||||
</Property>
|
||||
|
||||
</ObjectCollapsible>
|
||||
|
||||
</Property>
|
||||
@@ -2547,6 +2402,10 @@ Set if key is enabled or disabled. If disabled, the key cannot be used to verify
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/keys.updateKey"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"keyId":{"type":"ref","isRequired":true,"schema":"0"},"name":{"type":"ref","isRequired":false,"schema":"1"},"ownerId":{"type":"ref","isRequired":false,"schema":"2"},"meta":{"type":"ref","isRequired":false,"schema":"3"},"expires":{"type":"ref","isRequired":false,"schema":"4"},"ratelimit":{"type":"ref","isRequired":false,"schema":"5"},"remaining":{"type":"ref","isRequired":false,"schema":"10"},"refill":{"type":"ref","isRequired":false,"schema":"11"},"enabled":{"type":"ref","isRequired":false,"schema":"14"}}}} schemas={{"0":{"type":"string","defaultValue":"key_123","isRequired":false,"description":"The id of the key you want to modify"},"1":{"type":"string","defaultValue":"Customer X","isRequired":false,"description":"The name of the key"},"2":{"type":"string","defaultValue":"user_123","isRequired":false,"description":"The id of the tenant associated with this key. Use whatever reference you have in your system to identify the tenant. When verifying the key, we will send this field back to you, so you know who is accessing your API."},"3":{"type":"object","isRequired":false,"description":"Any additional metadata you want to store with the key","properties":{},"additionalProperties":true},"4":{"type":"number","defaultValue":0,"isRequired":false,"description":"The unix timestamp in milliseconds when the key will expire. If this field is null or undefined, the key is not expiring."},"5":{"type":"object","isRequired":false,"description":"Unkey comes with per-key ratelimiting out of the box. Set `null` to disable.","properties":{"type":{"type":"ref","isRequired":true,"schema":"6"},"limit":{"type":"ref","isRequired":true,"schema":"7"},"refillRate":{"type":"ref","isRequired":true,"schema":"8"},"refillInterval":{"type":"ref","isRequired":true,"schema":"9"}}},"6":{"type":"string","defaultValue":"","isRequired":false,"description":"Fast ratelimiting doesn't add latency, while consistent ratelimiting is more accurate."},"7":{"type":"number","defaultValue":"","isRequired":false,"description":"The total amount of burstable requests."},"8":{"type":"number","defaultValue":"","isRequired":false,"description":"How many tokens to refill during each refillInterval."},"9":{"type":"number","defaultValue":"","isRequired":false,"description":"Determines the speed at which tokens are refilled, in milliseconds."},"10":{"type":"number","defaultValue":1000,"isRequired":false,"description":"The number of requests that can be made with this key before it becomes invalid. Set `null` to disable."},"11":{"type":"object","isRequired":false,"description":"Unkey enables you to refill verifications for each key at regular intervals.","properties":{"interval":{"type":"ref","isRequired":true,"schema":"12"},"amount":{"type":"ref","isRequired":true,"schema":"13"}}},"12":{"type":"string","defaultValue":"","isRequired":false,"description":"Unkey will automatically refill verifications at the set interval. If null is used the refill functionality will be removed from the key."},"13":{"type":"number","defaultValue":"","isRequired":false,"description":"The amount of verifications to refill for each occurrence is determined individually for each key."},"14":{"type":"boolean","defaultValue":true,"isRequired":false,"description":"Set if key is enabled or disabled. If disabled, the key cannot be used to verify."}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -2984,7 +2843,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"POST"} route={"/v1/keys.updateRemaining"}>
|
||||
|
||||
## updateRemaining
|
||||
## Update Remaining
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -3031,6 +2890,10 @@ The value you want to set, add or subtract the remaining count by
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"POST"} route={"/v1/keys.updateRemaining"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"keyId":{"type":"ref","isRequired":true,"schema":"0"},"op":{"type":"ref","isRequired":true,"schema":"1"},"value":{"type":"ref","isRequired":true,"schema":"2"}}}} schemas={{"0":{"type":"string","defaultValue":"key_123","isRequired":false,"description":"The id of the key you want to modify"},"1":{"type":"string","defaultValue":"","isRequired":false,"description":"The operation you want to perform on the remaining count"},"2":{"type":"number","defaultValue":1,"isRequired":false,"description":"The value you want to set, add or subtract the remaining count by"}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
@@ -3455,7 +3318,7 @@ export interface Response {
|
||||
|
||||
<APIInfo method={"GET"} route={"/v1/keys.getVerifications"}>
|
||||
|
||||
## getVerifications
|
||||
## Get Verifications
|
||||
|
||||
### Authorization
|
||||
|
||||
@@ -3513,7 +3376,7 @@ The granularity of the usage data to fetch, currently only `day` is supported
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
| `200` | Usage numbers over time |
|
||||
| `200` | The configuration for a single key |
|
||||
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
|
||||
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
|
||||
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
|
||||
@@ -3522,6 +3385,10 @@ The granularity of the usage data to fetch, currently only `day` is supported
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"Authorization","defaultValue":"Bearer","isRequired":true,"description":"The Authorization access token"}} method={"GET"} route={"/v1/keys.getVerifications"} path={[]} query={[{"name":"keyId","type":"string","defaultValue":"key_1234","isRequired":false,"description":"The id of the key to fetch, either `keyId` or `ownerId` must be provided"},{"name":"ownerId","type":"string","defaultValue":"chronark","isRequired":false,"description":"The owner id to fetch keys for, either `keyId` or `ownerId` must be provided"},{"name":"start","type":"number","defaultValue":1620000000000,"isRequired":false,"description":"The start of the period to fetch usage for as unix milliseconds timestamp"},{"name":"end","type":"number","defaultValue":1620000000000,"isRequired":false,"description":"The end of the period to fetch usage for as unix milliseconds timestamp"},{"name":"granularity","type":"string","defaultValue":"day","isRequired":false,"description":"The granularity of the usage data to fetch, currently only `day` is supported"}]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
title: liveness
|
||||
title: Liveness
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-openapi/ui";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.unkey.dev"}>
|
||||
|
||||
<API>
|
||||
|
||||
<APIInfo method={"GET"} route={"/v1/liveness"}>
|
||||
|
||||
## v1.liveness
|
||||
## Check Liveness
|
||||
|
||||
| Status code | Description |
|
||||
| ----------- | ----------- |
|
||||
@@ -24,6 +24,10 @@ import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, Exa
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={undefined} method={"GET"} route={"/v1/liveness"} path={[]} query={[]} header={[]} body={undefined} schemas={{}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,9 +3,9 @@ title: ratelimits
|
||||
full: true
|
||||
---
|
||||
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
|
||||
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-ui/components/api";
|
||||
|
||||
<Root>
|
||||
<Root baseUrl={"https://api.unkey.dev"}>
|
||||
|
||||
<API>
|
||||
|
||||
@@ -159,6 +159,10 @@ Attach any metadata to this resources
|
||||
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
|
||||
| `500` | The server has encountered a situation it does not know how to handle. |
|
||||
|
||||
<APIPlayground authorization={{"type":"string","name":"authorization","defaultValue":"Bearer","isRequired":true,"description":"The authorization token"}} method={"POST"} route={"/v1/ratelimits.limit"} path={[]} query={[]} header={[]} body={{"type":"object","isRequired":true,"properties":{"namespace":{"type":"ref","isRequired":false,"schema":"0"},"identifier":{"type":"ref","isRequired":true,"schema":"1"},"limit":{"type":"ref","isRequired":true,"schema":"2"},"duration":{"type":"ref","isRequired":true,"schema":"3"},"cost":{"type":"ref","isRequired":false,"schema":"4"},"async":{"type":"ref","isRequired":false,"schema":"5"},"meta":{"type":"ref","isRequired":false,"schema":"6"},"resources":{"type":"ref","isRequired":false,"schema":"7"}}}} schemas={{"0":{"type":"string","defaultValue":"email.outbound","isRequired":false,"description":"Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes."},"1":{"type":"string","defaultValue":"user_123","isRequired":false,"description":"Identifier of your user, this can be their userId, an email, an ip or anything else."},"2":{"type":"number","defaultValue":10,"isRequired":false,"description":"How many requests may pass in a given window."},"3":{"type":"number","defaultValue":60000,"isRequired":false,"description":"The window duration in milliseconds"},"4":{"type":"number","defaultValue":2,"isRequired":false,"description":"Expensive requests may use up more tokens. You can specify a cost to the request here and we'll deduct this many tokens in the current window. \nIf there are not enough tokens left, the request is denied.\n \nSet it to 0 to receive the current limit without changing anything."},"5":{"type":"boolean","defaultValue":"","isRequired":false,"description":"Async will return a response immediately, lowering latency at the cost of accuracy."},"6":{"type":"object","isRequired":false,"description":"Attach any metadata to this request","properties":{}},"7":{"type":"array","description":"Resources that are about to be accessed by the user","isRequired":false,"items":"8"},"8":{"type":"object","isRequired":false,"properties":{"type":{"type":"ref","isRequired":true,"schema":"9"},"id":{"type":"ref","isRequired":true,"schema":"10"},"name":{"type":"ref","isRequired":false,"schema":"11"},"meta":{"type":"ref","isRequired":false,"schema":"12"}}},"9":{"type":"string","defaultValue":"organization","isRequired":false,"description":"The type of resource"},"10":{"type":"string","defaultValue":"org_123","isRequired":false,"description":"The unique identifier for the resource"},"11":{"type":"string","defaultValue":"unkey","isRequired":false,"description":"A human readable name for this resource"},"12":{"type":"object","isRequired":false,"description":"Attach any metadata to this resources","properties":{}}}}>
|
||||
|
||||
</APIPlayground>
|
||||
|
||||
</APIInfo>
|
||||
|
||||
<APIExample>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"extends": "tsconfig/base.json",
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"module": "ESNext",
|
||||
"target": "ES2020"
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
entry: ['./src/index.ts', './src/ui/index.ts'],
|
||||
format: 'esm',
|
||||
external: ['shiki'],
|
||||
dts: true,
|
||||
target: 'node18',
|
||||
target: 'es2022',
|
||||
});
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"lucide-react": "^0.400.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"react-medium-image-zoom": "^5.2.5",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -29,9 +29,7 @@ export const Accordions = forwardRef<
|
||||
const id = window.location.hash.substring(1);
|
||||
|
||||
if (id.length > 0)
|
||||
setValue((prev) => {
|
||||
return typeof prev === 'string' ? id : [id, ...prev];
|
||||
});
|
||||
setValue((prev) => (typeof prev === 'string' ? id : [id, ...prev]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -41,7 +39,7 @@ export const Accordions = forwardRef<
|
||||
ref={ref}
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
collapsible
|
||||
collapsible={type === 'single' ? true : undefined}
|
||||
className={cn(
|
||||
'divide-y divide-border overflow-hidden rounded-lg border bg-card',
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// TODO: Remove this (next major)
|
||||
'use client';
|
||||
import { type HTMLAttributes, type ReactNode } from 'react';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Tab, Tabs } from '@/components/tabs';
|
||||
import { Accordion, Accordions } from '@/components/accordion';
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-24 text-sm text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function API({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-2 xl:flex-row xl:items-start',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface APIInfoProps extends HTMLAttributes<HTMLDivElement> {
|
||||
method?: string;
|
||||
route: string;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
'rounded-lg border px-1.5 py-1 text-xs font-medium leading-[12px]',
|
||||
{
|
||||
variants: {
|
||||
color: {
|
||||
green:
|
||||
'border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400',
|
||||
yellow:
|
||||
'border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
|
||||
red: 'border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400',
|
||||
blue: 'border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400',
|
||||
orange:
|
||||
'border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function APIInfo({
|
||||
children,
|
||||
className,
|
||||
method = 'GET',
|
||||
route,
|
||||
...props
|
||||
}: APIInfoProps): React.ReactElement {
|
||||
let color: VariantProps<typeof badgeVariants>['color'] = 'green';
|
||||
if (['GET', 'HEAD'].includes(method)) color = 'green';
|
||||
if (['PUT'].includes(method)) color = 'yellow';
|
||||
if (['PATCH'].includes(method)) color = 'orange';
|
||||
if (['POST'].includes(method)) color = 'blue';
|
||||
if (['DELETE'].includes(method)) color = 'red';
|
||||
|
||||
return (
|
||||
<div className={cn('flex-1 prose-no-margin', className)} {...props}>
|
||||
<h2 className="flex flex-row items-center gap-3 rounded-lg border bg-card p-3 text-base">
|
||||
<span className={cn(badgeVariants({ color }))}>{method}</span>
|
||||
<code>{route}</code>
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PropertyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
deprecated: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Property({
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
deprecated,
|
||||
children,
|
||||
}: PropertyProps): React.ReactElement {
|
||||
return (
|
||||
<div className="mb-4 flex flex-col rounded-lg border bg-card p-3 prose-no-margin">
|
||||
<h4 className="inline-flex items-center gap-4">
|
||||
<code>{name}</code>
|
||||
{required ? (
|
||||
<div className={cn(badgeVariants({ color: 'red' }))}>Required</div>
|
||||
) : null}
|
||||
{deprecated ? (
|
||||
<div className={cn(badgeVariants({ color: 'yellow' }))}>
|
||||
Deprecated
|
||||
</div>
|
||||
) : null}
|
||||
<span className="ms-auto font-mono text-[13px] text-muted-foreground">
|
||||
{type}
|
||||
</span>
|
||||
</h4>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExample({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn('sticky top-6 prose-no-margin xl:w-[400px]', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Responses = Tabs;
|
||||
export const Response = Tab;
|
||||
|
||||
export const Requests = Tabs;
|
||||
export const Request = Tab;
|
||||
|
||||
export function ResponseTypes(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="!-m-4 border-none pt-2"
|
||||
defaultValue="Response"
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Response">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function TypeScriptResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Typescript">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function ObjectCollapsible(props: {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions type="single">
|
||||
<Accordion title={props.name}>{props.children}</Accordion>
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +1,7 @@
|
||||
'use client';
|
||||
import { type HTMLAttributes, type ReactNode } from 'react';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Tab, Tabs } from '@/components/tabs';
|
||||
import { Accordion, Accordions } from '@/components/accordion';
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-24 text-sm text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function API({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-2 xl:flex-row xl:items-start',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface APIInfoProps extends HTMLAttributes<HTMLDivElement> {
|
||||
method?: string;
|
||||
route: string;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
'rounded-lg border px-1.5 py-1 text-xs font-medium leading-[12px]',
|
||||
{
|
||||
variants: {
|
||||
color: {
|
||||
green:
|
||||
'border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400',
|
||||
yellow:
|
||||
'border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
|
||||
red: 'border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400',
|
||||
blue: 'border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400',
|
||||
orange:
|
||||
'border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function APIInfo({
|
||||
children,
|
||||
className,
|
||||
method = 'GET',
|
||||
route,
|
||||
...props
|
||||
}: APIInfoProps): React.ReactElement {
|
||||
let color: VariantProps<typeof badgeVariants>['color'] = 'green';
|
||||
if (['GET', 'HEAD'].includes(method)) color = 'green';
|
||||
if (['PUT'].includes(method)) color = 'yellow';
|
||||
if (['PATCH'].includes(method)) color = 'orange';
|
||||
if (['POST'].includes(method)) color = 'blue';
|
||||
if (['DELETE'].includes(method)) color = 'red';
|
||||
|
||||
return (
|
||||
<div className={cn('flex-1 prose-no-margin', className)} {...props}>
|
||||
<h2 className="flex flex-row items-center gap-3 rounded-lg border bg-card p-3 text-base">
|
||||
<span className={cn(badgeVariants({ color }))}>{method}</span>
|
||||
<code>{route}</code>
|
||||
</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PropertyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
deprecated: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Property({
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
deprecated,
|
||||
children,
|
||||
}: PropertyProps): React.ReactElement {
|
||||
return (
|
||||
<div className="mb-4 flex flex-col rounded-lg border bg-card p-3 prose-no-margin">
|
||||
<h4 className="inline-flex items-center gap-4">
|
||||
<code>{name}</code>
|
||||
{required ? (
|
||||
<div className={cn(badgeVariants({ color: 'red' }))}>Required</div>
|
||||
) : null}
|
||||
{deprecated ? (
|
||||
<div className={cn(badgeVariants({ color: 'yellow' }))}>
|
||||
Deprecated
|
||||
</div>
|
||||
) : null}
|
||||
<span className="ms-auto font-mono text-[13px] text-muted-foreground">
|
||||
{type}
|
||||
</span>
|
||||
</h4>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIExample({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn('sticky top-6 prose-no-margin xl:w-[400px]', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Responses = Tabs;
|
||||
export const Response = Tab;
|
||||
|
||||
export const Requests = Tabs;
|
||||
export const Request = Tab;
|
||||
|
||||
export function ResponseTypes(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions
|
||||
type="single"
|
||||
className="!-m-4 border-none pt-2"
|
||||
defaultValue="Response"
|
||||
>
|
||||
{props.children}
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Response">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function TypeScriptResponse(props: {
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return <Accordion title="Typescript">{props.children}</Accordion>;
|
||||
}
|
||||
|
||||
export function ObjectCollapsible(props: {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<Accordions type="single">
|
||||
<Accordion title={props.name}>{props.children}</Accordion>
|
||||
</Accordions>
|
||||
);
|
||||
}
|
||||
export * from '@/utils/use-on-change';
|
||||
export * from '@/utils/use-copy-button';
|
||||
export * from '@/utils/cn';
|
||||
export * from '@/theme/variants';
|
||||
export * from './api-legacy';
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import type { TabsContentProps } from '@radix-ui/react-tabs';
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
import type {
|
||||
TabsContentProps,
|
||||
TabsProps as BaseProps,
|
||||
} from '@radix-ui/react-tabs';
|
||||
import { useMemo, useState, useCallback, useLayoutEffect } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as Primitive from './ui/tabs';
|
||||
|
||||
@@ -39,7 +36,8 @@ function update(id: string, v: string, persist: boolean): void {
|
||||
else sessionStorage.setItem(id, v);
|
||||
}
|
||||
|
||||
export interface TabsProps {
|
||||
export interface TabsProps extends BaseProps {
|
||||
// TODO: Rename to group id so that it won't confuse with HTML `id` attribute (next major)
|
||||
/**
|
||||
* Identifier for Sharing value of tabs
|
||||
*/
|
||||
@@ -54,7 +52,6 @@ export interface TabsProps {
|
||||
defaultIndex?: number;
|
||||
|
||||
items?: string[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Tabs({
|
||||
@@ -62,7 +59,7 @@ export function Tabs({
|
||||
items = [],
|
||||
persist = false,
|
||||
defaultIndex = 0,
|
||||
children,
|
||||
...props
|
||||
}: TabsProps): React.ReactElement {
|
||||
const values = useMemo(() => items.map((item) => toValue(item)), [items]);
|
||||
const [value, setValue] = useState(values[defaultIndex]);
|
||||
@@ -100,7 +97,8 @@ export function Tabs({
|
||||
<Primitive.Tabs
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
className="my-4"
|
||||
{...props}
|
||||
className={cn('my-4', props.className)}
|
||||
>
|
||||
<Primitive.TabsList>
|
||||
{values.map((v, i) => (
|
||||
@@ -109,7 +107,7 @@ export function Tabs({
|
||||
</Primitive.TabsTrigger>
|
||||
))}
|
||||
</Primitive.TabsList>
|
||||
{children}
|
||||
{props.children}
|
||||
</Primitive.Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
|
||||
import { DirectionProvider } from '@radix-ui/react-direction';
|
||||
import type { ThemeProviderProps } from 'next-themes/dist/types';
|
||||
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
|
||||
import { SidebarProvider } from '@/contexts/sidebar';
|
||||
import { SidebarProvider } from './contexts/sidebar';
|
||||
import { SearchProvider, type SearchProviderProps } from './contexts/search';
|
||||
|
||||
interface SearchOptions
|
||||
|
||||
@@ -11,6 +11,7 @@ export const buttonVariants = cva(
|
||||
'border bg-secondary text-secondary-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
},
|
||||
size: {
|
||||
sm: 'gap-1 p-0.5 text-xs',
|
||||
icon: 'p-1.5 [&_svg]:size-5',
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+107
-15
@@ -687,18 +687,48 @@ importers:
|
||||
'@apidevtools/json-schema-ref-parser':
|
||||
specifier: ^11.6.4
|
||||
version: 11.6.4
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/[email protected])([email protected])
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
fast-glob:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.2
|
||||
fumadocs-ui:
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
json-schema-to-typescript:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
lucide-react:
|
||||
specifier: ^0.400.0
|
||||
version: 0.400.0([email protected])
|
||||
openapi-sampler:
|
||||
specifier: ^1.5.1
|
||||
version: 1.5.1
|
||||
react:
|
||||
specifier: '>= 18'
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: '>= 18'
|
||||
version: 18.3.1([email protected])
|
||||
react-hook-form:
|
||||
specifier: ^7.52.1
|
||||
version: 7.52.1([email protected])
|
||||
shiki:
|
||||
specifier: ^1.10.3
|
||||
version: 1.10.3
|
||||
swr:
|
||||
specifier: ^2.2.5
|
||||
version: 2.2.5([email protected])
|
||||
devDependencies:
|
||||
'@types/js-yaml':
|
||||
specifier: ^4.0.9
|
||||
@@ -709,9 +739,15 @@ importers:
|
||||
'@types/openapi-sampler':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@types/react':
|
||||
specifier: ^18.3.3
|
||||
version: 18.3.3
|
||||
eslint-config-custom:
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config-custom
|
||||
next:
|
||||
specifier: ^14.2.4
|
||||
version: 14.2.4(@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
|
||||
openapi-types:
|
||||
specifier: ^12.1.3
|
||||
version: 12.1.3
|
||||
@@ -848,6 +884,9 @@ importers:
|
||||
react-medium-image-zoom:
|
||||
specifier: ^5.2.5
|
||||
version: 5.2.5([email protected]([email protected]))([email protected])
|
||||
swr:
|
||||
specifier: ^2.2.5
|
||||
version: 2.2.5([email protected])
|
||||
tailwind-merge:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
@@ -2190,6 +2229,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
@@ -2306,6 +2358,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/[email protected]':
|
||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||
peerDependencies:
|
||||
@@ -3784,11 +3845,6 @@ packages:
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -5147,6 +5203,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -7501,6 +7563,35 @@ snapshots:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
'@radix-ui/react-collection': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-context': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-direction': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-dismissable-layer': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-focus-guards': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-focus-scope': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-id': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-popper': 1.2.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-portal': 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-slot': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-use-previous': 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-visually-hidden': 1.1.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1([email protected])
|
||||
react-remove-scroll: 2.5.7(@types/[email protected])([email protected])
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
@@ -7608,6 +7699,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.0
|
||||
@@ -9423,15 +9520,6 @@ snapshots:
|
||||
minipass: 7.1.2
|
||||
path-scurry: 1.11.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
foreground-child: 3.2.1
|
||||
jackspeak: 3.4.0
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.0
|
||||
path-scurry: 1.11.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
foreground-child: 3.2.1
|
||||
@@ -9976,7 +10064,7 @@ snapshots:
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.6
|
||||
cli-color: 2.0.4
|
||||
glob: 10.4.2
|
||||
glob: 10.4.1
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.1.0
|
||||
lodash: 4.17.21
|
||||
@@ -11141,6 +11229,10 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
Reference in New Issue
Block a user