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:
Kyrylo Lvov
2024-07-14 21:02:36 +08:00
committed by GitHub
co-authored by fuma-nama Fuma Nama
parent c79f42a332
commit ad143e19f5
59 changed files with 3281 additions and 2633 deletions
+30
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Implement OpenAPI playground
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support passing base url to Root component
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Combine `allOf` into one object schema
+10
View File
@@ -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.
+35 -3
View File
@@ -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,
);
+5 -2
View File
@@ -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
+2 -2
View File
@@ -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,
);
+1
View File
@@ -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({
+2 -1
View File
@@ -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',
},
};
+36 -1
View File
@@ -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"
}
+14 -6
View File
@@ -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;
}),
);
+14 -9
View File
@@ -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'}\``,
],
),
);
+282
View File
@@ -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,
};
}
+16 -2
View File
@@ -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),
};
+51 -3
View File
@@ -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';
+224
View File
@@ -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>
);
}
+138
View File
@@ -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,
};
+70
View File
@@ -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;
}
+164
View File
@@ -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,
};
}
+12
View File
@@ -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;
+544
View File
@@ -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>
);
}
+214
View File
@@ -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>
);
}
+92
View File
@@ -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,
};
}
@@ -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;
}
+11
View File
@@ -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('');
}
+7
View File
@@ -0,0 +1,7 @@
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/ui/**/*.{ts,tsx}'],
presets: [createPreset()],
};
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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();
+22 -2
View File
@@ -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>
+10 -2
View File
@@ -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>
+14 -2
View File
@@ -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>
+495 -68
View File
@@ -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>
+69 -202
View File
@@ -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>
+8 -4
View File
@@ -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>
+2 -4
View File
@@ -1,11 +1,9 @@
{
"extends": "tsconfig/base.json",
"extends": "tsconfig/react-library.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
},
"module": "ESNext",
"target": "ES2020"
}
},
"exclude": ["node_modules", "dist"]
}
+3 -2
View File
@@ -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',
});
+1
View File
@@ -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": {
+2 -4
View File
@@ -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,
+186
View File
@@ -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>
);
}
+5 -183
View File
@@ -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';
+11 -13
View File
@@ -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>
);
}
+1 -1
View File
@@ -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
+1
View File
@@ -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',
},
},
+107 -15
View File
@@ -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]: {}