OpenAPI: Support generating index.mdx (#2245)

* OpenAPI: Support generating index.mdx

Motivation: Some openapi specifications have detailed descriptions that need to be displayed, however
generateFiles with per operation loses that description.  By using generateIndex option, we can leverage it as
the index.mdx of the folder of the generated API docs.

* Enhances generateFiles with additional boolean option generateIndex
* Adds isIndex parameter to name function
* Create generateIndex option to generate index containing title and index only APIPage

* Add changeset for openapi index file enhancement

* Remove accidental debugger

* Docs: Update integrations/openapi with generateIndex changes

* OpenAPI: Simplify generateIndex to index options

* Update to use index based on PR suggestions
* Reuse imports to add imports for getPageTreePeers and source
* Update tests

* Update documentation

* redesign `index` option

---------

Co-authored-by: Fuma Nama <[email protected]>
This commit is contained in:
Noel Madali
2025-08-27 17:08:12 +08:00
committed by GitHub
co-authored by Fuma Nama
parent 5f2ec6e06c
commit 22371ce67c
16 changed files with 584 additions and 2501 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Support `index` generation in `generateFiles`: create index files to link all generated pages.
@@ -74,6 +74,77 @@ void generateFiles({
});
```
### Index
Generate index files with cards linking to generated pages.
```ts
import { generateFiles } from 'fumadocs-openapi';
void generateFiles({
input: ['./petstore.yaml', './museum.yaml'],
output: './content/docs',
index: {
// for generating `href`
url: {
baseUrl: '/docs',
contentDir: './content/docs',
},
items: [
{
path: 'index.mdx',
// optional: restrict the input files
only: ['petstore.yaml'],
// optional: set frontmatter
description: 'All available pages',
},
],
},
});
```
### Imports
Add custom imports to generated MDX files. This is useful for including components, utilities, or other dependencies in your generated documentation.
```ts
import { generateFiles } from 'fumadocs-openapi';
void generateFiles({
input: ['./petstore.yaml'],
output: './content/docs',
imports: [
{
names: ['CustomComponent', 'AnotherComponent'],
from: '@/components/custom',
},
{
names: ['utils'],
from: '@/lib/utils',
},
{
names: ['API_BASE_URL'],
from: '@/constants',
},
],
});
```
This will add the following imports to all generated MDX files:
```ts
import { CustomComponent, AnotherComponent } from '@/components/custom';
import { utils } from '@/lib/utils';
import { API_BASE_URL } from '@/constants';
```
<Callout type="info">
The `imports` configuration is especially important for Cards generation in
index files, where you need to import `source` and `getPageTreePeers` for the
navigation functionality to work.
</Callout>
### Name
A function that controls the output path of MDX pages.
+1
View File
@@ -63,6 +63,7 @@
"fumadocs-core": "workspace:*",
"fumadocs-ui": "workspace:*",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"hast-util-to-jsx-runtime": "^2.3.6",
"js-yaml": "^4.1.0",
"next-themes": "^0.4.6",
+249 -93
View File
@@ -3,6 +3,7 @@ import * as path from 'node:path';
import { glob } from 'tinyglobby';
import {
generateAll,
generateDocument,
type GenerateOptions,
type GeneratePageOutput,
generatePages,
@@ -14,6 +15,8 @@ import {
type ProcessedDocument,
} from '@/utils/process-document';
import type { OpenAPIServer } from '@/server';
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
import matter from 'gray-matter';
interface GenerateFileOutput {
/**
@@ -24,6 +27,11 @@ interface GenerateFileOutput {
content: string;
}
interface OutputFile {
path: string;
content: string;
}
interface OperationConfig extends BaseConfig {
/**
* Generate a page for each API endpoint/operation (default).
@@ -99,6 +107,16 @@ interface BaseName {
algorithm?: 'v2' | 'v1';
}
interface IndexItem {
path: string;
title?: string;
description?: string;
/**
* Only include items from specific input schema ids
*/
only?: string[];
}
interface BaseConfig extends GenerateOptions {
/**
* Schema files, or the OpenAPI server object
@@ -116,10 +134,49 @@ interface BaseConfig extends GenerateOptions {
* By default, it only escapes whitespaces and upper case (English) characters
*/
slugify?: (name: string) => string;
/**
* Generate index files with cards linking to generated pages.
*/
index?: {
items: IndexItem[];
/**
* Generate URLs for cards
*/
url:
| ((filePath: string) => string)
| {
baseUrl: string;
/**
* Base content directory
*/
contentDir: string;
};
};
/**
* Can add/change/remove output files before writing to file system
**/
beforeWrite?: (files: OutputFile[]) => void | Promise<void>;
}
export async function generateFiles(options: Config): Promise<void> {
const { cwd = process.cwd() } = options;
const files = await generateFilesOnly(options);
await Promise.all(
files.map(async (file) => {
await mkdir(path.dirname(file.path), { recursive: true });
await writeFile(file.path, file.content);
console.log(`Generated: ${file.path}`);
}),
);
}
export async function generateFilesOnly(
options: Config,
): Promise<OutputFile[]> {
const { cwd = process.cwd(), beforeWrite } = options;
const input =
typeof options.input === 'string' ? [options.input] : options.input;
let schemas: Record<string, ProcessedDocument> = {};
@@ -145,23 +202,34 @@ export async function generateFiles(options: Config): Promise<void> {
}
const resolvedSchemas = Object.entries(schemas);
if (resolvedSchemas.length === 0) {
throw new Error('No input files found.');
}
await Promise.all(
resolvedSchemas.map(([id, document]) =>
generateFromDocument(id, document, options),
),
);
const documentFiles = new Map<string, OutputFile[]>();
const files: OutputFile[] = [];
for (const [id, schema] of resolvedSchemas) {
const result = generateFromDocument(id, schema, options);
files.push(...result);
documentFiles.set(id, result);
}
if (options.index) {
files.push(...generateIndexFiles(documentFiles, options));
}
beforeWrite?.(files);
return files;
}
async function generateFromDocument(
function generateFromDocument(
schemaId: string,
document: ProcessedDocument,
processed: ProcessedDocument,
options: Config,
) {
): OutputFile[] {
const files: OutputFile[] = [];
const { document } = processed;
const { output, cwd = process.cwd(), slugify = defaultSlugify } = options;
const outputDir = path.join(cwd, output);
@@ -171,54 +239,14 @@ async function generateFromDocument(
) => string;
if (!options.name || typeof options.name !== 'function') {
const { algorithm = 'v2' } = options.name ?? {};
const algorithm = options.name?.algorithm;
nameFn = (output, document) => {
if (options.per === 'tag') {
const result = output as GenerateTagOutput;
return slugify(result.tag);
}
if (options.per === 'file') {
return isUrl(schemaId)
? 'index'
: path.basename(schemaId, path.extname(schemaId));
}
const result = output as GeneratePageOutput;
if (result.type === 'operation') {
const operation =
document.paths![result.item.path]![result.item.method]!;
if (algorithm === 'v2' && operation.operationId) {
return operation.operationId;
}
return path.join(
getOutputPathFromRoute(result.item.path),
result.item.method.toLowerCase(),
);
}
const hook = document.webhooks![result.item.name][result.item.method]!;
if (algorithm === 'v2' && hook.operationId) {
return hook.operationId;
}
return slugify(result.item.name);
};
nameFn = (out, doc) =>
defaultNameFn(schemaId, out, doc, options, algorithm);
} else {
nameFn = options.name as typeof nameFn;
}
async function write(file: string, content: string) {
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, content);
}
function getOutputPaths(
groupBy: OperationConfig['groupBy'] = 'none',
result: GeneratePageOutput,
@@ -234,14 +262,12 @@ async function generateFromDocument(
];
}
const file = nameFn(result, document.document);
const file = nameFn(result, document);
if (groupBy === 'tag') {
let tags =
result.type === 'operation'
? document.document.paths![result.item.path]![result.item.method]!
.tags
: document.document.webhooks![result.item.name][result.item.method]!
.tags;
? document.paths![result.item.path]![result.item.method]!.tags
: document.webhooks![result.item.name][result.item.method]!.tags;
if (!tags || tags.length === 0) {
console.warn(
@@ -258,58 +284,113 @@ async function generateFromDocument(
}
if (options.per === 'file') {
const result = await generateAll(schemaId, document, options);
const result = generateAll(schemaId, processed, options);
const filename = nameFn(
{
pathOrUrl: schemaId,
content: result,
},
document.document,
document,
);
const outPath = path.join(outputDir, `${filename}.mdx`);
files.push({
path: path.join(outputDir, `${filename}.mdx`),
content: result,
});
return files;
}
await write(outPath, result);
console.log(`Generated: ${outPath}`);
} else if (options.per === 'tag') {
const results = await generateTags(schemaId, document, options);
if (options.per === 'tag') {
const results = generateTags(schemaId, processed, options);
for (const result of results) {
const filename = nameFn(result, document.document);
const outPath = path.join(outputDir, `${filename}.mdx`);
await write(outPath, result.content);
console.log(`Generated: ${outPath}`);
}
} else {
const results = await generatePages(schemaId, document, options);
const mapping = new Map<string, GeneratePageOutput>();
const filename = nameFn(result, document);
for (const result of results) {
for (const outputPath of getOutputPaths(options.groupBy, result)) {
mapping.set(outputPath, result);
}
files.push({
path: path.join(outputDir, `${filename}.mdx`),
content: result.content,
});
}
for (const [key, output] of mapping.entries()) {
let outputPath = key;
return files;
}
// v1 will remove nested directories
if (typeof options.name === 'object' && options.name.algorithm === 'v1') {
const isSharedDir = Array.from(mapping.keys()).some(
(item) =>
item !== outputPath &&
path.dirname(item) === path.dirname(outputPath),
);
const results = generatePages(schemaId, processed, options);
const mapping = new Map<string, GeneratePageOutput>();
if (!isSharedDir && path.dirname(outputPath) !== '.') {
outputPath = path.join(path.dirname(outputPath) + '.mdx');
}
}
await write(path.join(outputDir, outputPath), output.content);
console.log(`Generated: ${outputPath}`);
for (const result of results) {
for (const outputPath of getOutputPaths(options.groupBy, result)) {
mapping.set(outputPath, result);
}
}
for (const [key, output] of mapping.entries()) {
let outputPath = key;
// v1 will remove nested directories
if (typeof options.name === 'object' && options.name.algorithm === 'v1') {
const isSharedDir = Array.from(mapping.keys()).some(
(item) =>
item !== outputPath &&
path.dirname(item) === path.dirname(outputPath),
);
if (!isSharedDir && path.dirname(outputPath) !== '.') {
outputPath = path.join(path.dirname(outputPath) + '.mdx');
}
}
files.push({
path: path.join(outputDir, outputPath),
content: output.content,
});
}
return files;
}
function defaultNameFn(
schemaId: string,
output: GeneratePageOutput | GenerateTagOutput | GenerateFileOutput,
document: ProcessedDocument['document'],
options: Config,
algorithm: 'v2' | 'v1' = 'v2',
) {
const { slugify = defaultSlugify } = options;
if (options.per === 'tag') {
const result = output as GenerateTagOutput;
return slugify(result.tag);
}
if (options.per === 'file') {
return isUrl(schemaId)
? 'index'
: path.basename(schemaId, path.extname(schemaId));
}
const result = output as GeneratePageOutput;
if (result.type === 'operation') {
const operation = document.paths![result.item.path]![result.item.method]!;
if (algorithm === 'v2' && operation.operationId) {
return operation.operationId;
}
return path.join(
getOutputPathFromRoute(result.item.path),
result.item.method.toLowerCase(),
);
}
const hook = document.webhooks![result.item.name][result.item.method]!;
if (algorithm === 'v2' && hook.operationId) {
return hook.operationId;
}
return slugify(result.item.name);
}
function isUrl(input: string): boolean {
@@ -330,6 +411,81 @@ function getOutputPathFromRoute(path: string): string {
);
}
function generateIndexFiles(
generatedFiles: Map<string, OutputFile[]>,
options: Config,
): OutputFile[] {
const files: OutputFile[] = [];
const { index, output, cwd = process.cwd() } = options;
if (!index) return files;
const { items, url } = index;
let urlFn: (path: string) => string;
if (typeof url === 'object') {
const getUrl = createGetUrl(url.baseUrl);
const contentDir = path.resolve(cwd, url.contentDir);
urlFn = (file) => getUrl(getSlugs(path.relative(contentDir, file)));
} else {
urlFn = url;
}
function fileContent(item: IndexItem): string {
const content: string[] = [];
content.push('<Cards>');
const files: OutputFile[] = [];
if (item.only) {
for (let id of item.only) {
if (id.startsWith('./')) id = id.slice(2);
const result = generatedFiles.get(id);
if (!result)
throw new Error(
`${id} does not exist on "input", available: ${Array.from(generatedFiles.keys())}.`,
);
files.push(...result);
}
} else {
for (const value of generatedFiles.values()) files.push(...value);
}
for (const file of files) {
const isContent = file.path.endsWith('.mdx') || file.path.endsWith('.md');
if (!isContent) continue;
const { data } = matter(file.content);
if (typeof data.title !== 'string') continue;
content.push(
`<Card href="${urlFn(file.path)}" title=${JSON.stringify(data.title)} description=${JSON.stringify(data.description)} />`,
);
}
content.push('</Cards>');
return generateDocument(
{
title: item.title ?? 'Overview',
description: item.description,
},
content.join('\n'),
options,
);
}
const outputDir = path.join(cwd, output);
for (const item of items) {
files.push({
path: path.join(
outputDir,
path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
),
content: fileContent(item),
});
}
return files;
}
function defaultSlugify(s: string): string {
return s.replace(/\s+/g, '-').toLowerCase();
}
+179 -15
View File
@@ -1,11 +1,17 @@
import { getAPIPageItems } from '@/build-routes';
import {
type DocumentContext,
generateDocument,
} from '@/utils/generate-document';
import { idToTitle } from '@/utils/id-to-title';
import type { OperationItem, WebhookItem } from '@/render/api-page';
import type {
ApiPageProps,
OperationItem,
WebhookItem,
} from '@/render/api-page';
import type { ProcessedDocument } from '@/utils/process-document';
import type { TableOfContents } from 'fumadocs-core/server';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type { Document, TagObject } from '@/types';
import { dump } from 'js-yaml';
import type { NoReference } from '@/utils/schema';
import Slugger from 'github-slugger';
export interface GenerateOptions {
/**
@@ -74,15 +80,15 @@ export type GeneratePageOutput =
content: string;
};
export async function generateAll(
export function generateAll(
schemaId: string,
processed: ProcessedDocument,
options: GenerateOptions = {},
): Promise<string> {
): string {
const { document } = processed;
const items = getAPIPageItems(document);
return generateDocument(
return generatePage(
schemaId,
processed,
{
@@ -101,11 +107,11 @@ export async function generateAll(
);
}
export async function generatePages(
export function generatePages(
schemaId: string,
processed: ProcessedDocument,
options: GenerateOptions = {},
): Promise<GeneratePageOutput[]> {
): GeneratePageOutput[] {
const { document } = processed;
const items = getAPIPageItems(document);
const result: GeneratePageOutput[] = [];
@@ -119,7 +125,7 @@ export async function generatePages(
result.push({
type: 'operation',
item,
content: generateDocument(
content: generatePage(
schemaId,
processed,
{
@@ -150,7 +156,7 @@ export async function generatePages(
result.push({
type: 'webhook',
item,
content: generateDocument(
content: generatePage(
schemaId,
processed,
{
@@ -172,11 +178,11 @@ export async function generatePages(
return result;
}
export async function generateTags(
export function generateTags(
schemaId: string,
processed: ProcessedDocument,
options: GenerateOptions = {},
): Promise<GenerateTagOutput[]> {
): GenerateTagOutput[] {
const { document } = processed;
if (!document.tags) return [];
const items = getAPIPageItems(document);
@@ -196,7 +202,7 @@ export async function generateTags(
return {
tag: tag.name,
content: generateDocument(
content: generatePage(
schemaId,
processed,
{
@@ -217,3 +223,161 @@ export async function generateTags(
} satisfies GenerateTagOutput;
});
}
export function generateDocument(
frontmatter: unknown,
content: string,
options: Pick<GenerateOptions, 'addGeneratedComment' | 'imports'>,
): string {
const { addGeneratedComment = true, imports } = options;
const out: string[] = [];
const banner = dump(frontmatter).trim();
if (banner.length > 0) out.push(`---\n${banner}\n---`);
if (addGeneratedComment) {
let commentContent =
'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';
if (typeof addGeneratedComment === 'string') {
commentContent = addGeneratedComment;
}
commentContent = commentContent.replaceAll('/', '\\/');
out.push(`{/* ${commentContent} */}`);
}
if (imports) {
out.push(
...imports
.map(
(item) =>
`import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`,
)
.join('\n'),
);
}
out.push(content);
return out.join('\n\n');
}
interface StaticData {
toc: TableOfContents;
structuredData: StructuredData;
}
export type DocumentContext =
| {
type: 'tag';
tag: TagObject | undefined;
}
| {
type: 'operation';
}
| {
type: 'file';
};
function generatePage(
schemaId: string,
processed: ProcessedDocument,
pageProps: Omit<ApiPageProps, 'document'>,
options: GenerateOptions & {
title: string;
description?: string;
},
context: DocumentContext,
): string {
const { frontmatter, includeDescription = false } = options;
const extend = frontmatter?.(options.title, options.description, context);
const page: ApiPageProps = {
...pageProps,
document: schemaId,
};
let meta: object | undefined;
if (page.operations?.length === 1) {
const operation = page.operations[0];
meta = {
method: operation.method.toUpperCase(),
route: operation.path,
};
}
const data = generateStaticData(processed.document, page);
const content: string[] = [];
if (options.description && includeDescription)
content.push(options.description);
content.push(pageContent(page));
return generateDocument(
{
title: options.title,
description: !includeDescription ? options.description : undefined,
full: true,
...extend,
_openapi: {
...meta,
...data,
...(extend?._openapi as object | undefined),
},
},
content.join('\n\n'),
options,
);
}
function generateStaticData(
dereferenced: NoReference<Document>,
props: ApiPageProps,
): StaticData {
const slugger = new Slugger();
const toc: TableOfContents = [];
const structuredData: StructuredData = { headings: [], contents: [] };
for (const item of props.operations ?? []) {
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation) continue;
if (props.hasHead && operation.operationId) {
const title =
operation.summary ??
(operation.operationId ? idToTitle(operation.operationId) : item.path);
const id = slugger.slug(title);
toc.push({
depth: 2,
title,
url: `#${id}`,
});
structuredData.headings.push({
content: title,
id,
});
}
if (operation.description)
structuredData.contents.push({
content: operation.description,
heading: structuredData.headings.at(-1)?.id,
});
}
return { toc, structuredData };
}
function pageContent(props: ApiPageProps): string {
// filter extra properties in props
const operations: OperationItem[] = (props.operations ?? []).map((item) => ({
path: item.path,
method: item.method,
}));
const webhooks: WebhookItem[] = (props.webhooks ?? []).map((item) => ({
name: item.name,
method: item.method,
}));
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
}
@@ -1,161 +0,0 @@
import { dump } from 'js-yaml';
import Slugger from 'github-slugger';
import type { TableOfContents } from 'fumadocs-core/server';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
import type {
ApiPageProps,
OperationItem,
WebhookItem,
} from '@/render/api-page';
import type { GenerateOptions } from '@/generate';
import { idToTitle } from '@/utils/id-to-title';
import type { Document, TagObject } from '@/types';
import type { NoReference } from '@/utils/schema';
import type { ProcessedDocument } from '@/utils/process-document';
interface StaticData {
toc: TableOfContents;
structuredData: StructuredData;
}
export type DocumentContext =
| {
type: 'tag';
tag: TagObject | undefined;
}
| {
type: 'operation';
}
| {
type: 'file';
};
export function generateDocument(
schemaId: string,
processed: ProcessedDocument,
pageProps: Omit<ApiPageProps, 'document'>,
options: GenerateOptions & {
title: string;
description?: string;
},
context: DocumentContext,
): string {
const {
frontmatter,
includeDescription = false,
addGeneratedComment = true,
} = options;
const out: string[] = [];
const extend = frontmatter?.(options.title, options.description, context);
const page: ApiPageProps = {
...pageProps,
document: schemaId,
};
let meta: object | undefined;
if (page.operations?.length === 1) {
const operation = page.operations[0];
meta = {
method: operation.method.toUpperCase(),
route: operation.path,
};
}
const data = generateStaticData(processed.document, page);
const banner = dump({
title: options.title,
description: !includeDescription ? options.description : undefined,
full: true,
...extend,
_openapi: {
...meta,
...data,
...(extend?._openapi as object | undefined),
},
}).trim();
if (banner.length > 0) out.push(`---\n${banner}\n---`);
if (addGeneratedComment !== false) {
let commentContent =
'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';
if (typeof addGeneratedComment === 'string') {
commentContent = addGeneratedComment;
}
commentContent = commentContent.replaceAll('/', '\\/');
out.push(`{/* ${commentContent} */}`);
}
const imports = options.imports
?.map(
(item) =>
`import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`,
)
.join('\n');
if (imports) {
out.push(imports);
}
if (options.description && includeDescription) out.push(options.description);
out.push(pageContent(page));
return out.join('\n\n');
}
function generateStaticData(
dereferenced: NoReference<Document>,
props: ApiPageProps,
): StaticData {
const slugger = new Slugger();
const toc: TableOfContents = [];
const structuredData: StructuredData = { headings: [], contents: [] };
for (const item of props.operations ?? []) {
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation) continue;
if (props.hasHead && operation.operationId) {
const title =
operation.summary ??
(operation.operationId ? idToTitle(operation.operationId) : item.path);
const id = slugger.slug(title);
toc.push({
depth: 2,
title,
url: `#${id}`,
});
structuredData.headings.push({
content: title,
id,
});
}
if (operation.description)
structuredData.contents.push({
content: operation.description,
heading: structuredData.headings.at(-1)?.id,
});
}
return { toc, structuredData };
}
function pageContent(props: ApiPageProps): string {
// filter extra properties in props
const operations: OperationItem[] = (props.operations ?? []).map((item) => ({
path: item.path,
method: item.method,
}));
const webhooks: WebhookItem[] = (props.webhooks ?? []).map((item) => ({
name: item.name,
method: item.method,
}));
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
}
+38 -46
View File
@@ -2,7 +2,7 @@ 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 { generateFiles } from '../src';
import { generateFilesOnly } from '../src';
import { processDocument } from '@/utils/process-document';
import { generateAll, generateTags } from '@/generate';
@@ -22,7 +22,7 @@ describe('Generate documents', () => {
});
test('Pet Store', async () => {
const result = await generateAll(
const result = generateAll(
'./fixtures/petstore.yaml',
await processDocument(join(cwd, './fixtures/petstore.yaml')),
{
@@ -34,7 +34,7 @@ describe('Generate documents', () => {
});
test('Museum', async () => {
const tags = await generateTags(
const tags = generateTags(
'./fixtures/museum.yaml',
await processDocument(join(cwd, './fixtures/museum.yaml')),
{
@@ -50,7 +50,7 @@ describe('Generate documents', () => {
});
test('Unkey', async () => {
const tags = await generateTags(
const tags = generateTags(
'./fixtures/unkey.json',
await processDocument(join(cwd, './fixtures/unkey.json')),
{ cwd },
@@ -63,44 +63,22 @@ describe('Generate documents', () => {
}
});
vi.mock('node:fs/promises', async (importOriginal) => {
return {
...(await importOriginal<typeof import('node:fs/promises')>()),
mkdir: vi.fn().mockImplementation(() => {
// do nothing
}),
writeFile: vi.fn().mockImplementation(() => {
// do nothing
}),
};
});
test('Generate Files', async () => {
await generateFiles({
const out = await generateFilesOnly({
input: ['./fixtures/museum.yaml', './fixtures/petstore.yaml'],
output: './out',
per: 'file',
cwd,
});
const fs = await import('node:fs/promises');
expect(fs.writeFile).toBeCalledTimes(2);
expect(fs.writeFile).toBeCalledWith(
join(cwd, './out/museum.mdx'),
expect.anything(),
await expect(JSON.stringify(out, null, 2)).toMatchFileSnapshot(
'./out/petstore.json',
);
expect(fs.writeFile).toBeCalledWith(
join(cwd, './out/petstore.mdx'),
expect.anything(),
);
expect(fs.mkdir).toBeCalledWith(join(cwd, './out'), expect.anything());
});
test('Generate Files - throws error when no input files found', async () => {
await expect(
generateFiles({
generateFilesOnly({
input: ['./fixtures/non-existent-*.yaml'],
output: './out',
per: 'file',
@@ -111,7 +89,7 @@ describe('Generate documents', () => {
);
await expect(
generateFiles({
generateFilesOnly({
input: [
'./fixtures/non-existent-1.yaml',
'./fixtures/non-existent-2.yaml',
@@ -129,7 +107,7 @@ describe('Generate documents', () => {
});
test('Generate Files - groupBy tag per operation', async () => {
await generateFiles({
const out = await generateFilesOnly({
input: ['./fixtures/products.yaml'],
output: './out',
per: 'operation',
@@ -140,23 +118,37 @@ describe('Generate documents', () => {
cwd,
});
const fs = await import('node:fs/promises');
expect(fs.writeFile).toBeCalledTimes(3);
expect(fs.writeFile).toBeCalledWith(
join(cwd, './out/products/products/productid.mdx'),
expect.anything(),
await expect(JSON.stringify(out, null, 2)).toMatchFileSnapshot(
'./out/products-per-tag.json',
);
});
expect(fs.writeFile).toBeCalledWith(
join(cwd, './out/inventory/inventory/productid.mdx'),
expect.anything(),
);
test('Generate Files - with index', async () => {
const out = await generateFilesOnly({
input: ['./fixtures/products.yaml'],
output: './out',
per: 'operation',
name: {
algorithm: 'v1',
},
index: {
url: {
baseUrl: '/docs',
contentDir: './out',
},
items: [
{
description: 'all available pages',
path: 'index.mdx',
only: ['./fixtures/products.yaml'],
},
],
},
cwd,
});
expect(fs.writeFile).toBeCalledWith(
join(cwd, './out/products/inventory/productid.mdx'),
expect.anything(),
await expect(JSON.stringify(out, null, 2)).toMatchFileSnapshot(
'./out/products-with-index.json',
);
});
});
+10
View File
@@ -0,0 +1,10 @@
[
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/museum.mdx",
"content": "---\ntitle: Redocly Museum API\ndescription: >-\n An imaginary, but delightful Museum API for interacting with museum services\n and information. Built with love by Redocly.\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: Get museum hours\n url: '#get-museum-hours'\n - depth: 2\n title: List special events\n url: '#list-special-events'\n - depth: 2\n title: Create special events\n url: '#create-special-events'\n - depth: 2\n title: Get special event\n url: '#get-special-event'\n - depth: 2\n title: Update special event\n url: '#update-special-event'\n - depth: 2\n title: Delete special event\n url: '#delete-special-event'\n - depth: 2\n title: Buy museum tickets\n url: '#buy-museum-tickets'\n - depth: 2\n title: Get ticket QR code\n url: '#get-ticket-qr-code'\n structuredData:\n headings:\n - content: Get museum hours\n id: get-museum-hours\n - content: List special events\n id: list-special-events\n - content: Create special events\n id: create-special-events\n - content: Get special event\n id: get-special-event\n - content: Update special event\n id: update-special-event\n - content: Delete special event\n id: delete-special-event\n - content: Buy museum tickets\n id: buy-museum-tickets\n - content: Get ticket QR code\n id: get-ticket-qr-code\n contents:\n - content: Get upcoming museum operating hours\n heading: get-museum-hours\n - content: Return a list of upcoming special events at the museum.\n heading: list-special-events\n - content: Creates a new special event for the museum.\n heading: create-special-events\n - content: Get details about a special event.\n heading: get-special-event\n - content: Update the details of a special event\n heading: update-special-event\n - content: >-\n Delete a special event from the collection. Allows museum to cancel\n planned events.\n heading: delete-special-event\n - content: Purchase museum tickets for general entry or special events.\n heading: buy-museum-tickets\n - content: >-\n Return an image of your ticket with scannable QR code. Used for event\n entry.\n heading: get-ticket-qr-code\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/museum.yaml\"} operations={[{\"path\":\"/museum-hours\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"post\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"get\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"patch\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"delete\"},{\"path\":\"/tickets\",\"method\":\"post\"},{\"path\":\"/tickets/{ticketId}/qr\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
},
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/petstore.mdx",
"content": "---\ntitle: Swagger Petstore\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: List all pets\n url: '#list-all-pets'\n - depth: 2\n title: Create a pet\n url: '#create-a-pet'\n - depth: 2\n title: Info for a specific pet\n url: '#info-for-a-specific-pet'\n structuredData:\n headings:\n - content: List all pets\n id: list-all-pets\n - content: Create a pet\n id: create-a-pet\n - content: Info for a specific pet\n id: info-for-a-specific-pet\n contents: []\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/petstore.yaml\"} operations={[{\"path\":\"/pets\",\"method\":\"get\"},{\"path\":\"/pets\",\"method\":\"post\"},{\"path\":\"/pets/{petId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
}
]
@@ -0,0 +1,14 @@
[
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/products/products/productid.mdx",
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
},
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/inventory/inventory/productid.mdx",
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
},
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/products/inventory/productid.mdx",
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
}
]
@@ -0,0 +1,14 @@
[
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/products/productid.mdx",
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
},
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/inventory/productid.mdx",
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
},
{
"path": "/Users/xred/dev/fumadocs/packages/openapi/test/out/index.mdx",
"content": "---\ntitle: Overview\ndescription: all available pages\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<Cards>\n<Card href=\"/docs/products/productid\" title=\"Get product details\" description=\"Retrieve details of a product by its ID.\" />\n<Card href=\"/docs/inventory/productid\" title=\"Get product inventory\" description=\"Retrieve inventory details for a specific product.\" />\n</Cards>"
}
]
-31
View File
@@ -1,31 +0,0 @@
---
title: Apis
full: true
_openapi:
toc:
- depth: 2
title: Get Api
url: '#get-api'
- depth: 2
title: Create Api
url: '#create-api'
- depth: 2
title: List Keys
url: '#list-keys'
- depth: 2
title: Delete Api
url: '#delete-api'
structuredData:
headings:
- content: Get Api
id: get-api
- content: Create Api
id: create-api
- content: List Keys
id: list-keys
- content: Delete Api
id: delete-api
contents: []
---
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/apis.getApi"},{"method":"post","path":"/v1/apis.createApi"},{"method":"get","path":"/v1/apis.listKeys"},{"method":"post","path":"/v1/apis.deleteApi"}]} hasHead={true} />
-46
View File
@@ -1,46 +0,0 @@
---
title: Keys
full: true
_openapi:
toc:
- depth: 2
title: Get Key
url: '#get-key'
- depth: 2
title: Delete Key
url: '#delete-key'
- depth: 2
title: Create Key
url: '#create-key'
- depth: 2
title: Verify Key
url: '#verify-key'
- depth: 2
title: Update Key
url: '#update-key'
- depth: 2
title: Update Remaining
url: '#update-remaining'
- depth: 2
title: Get Verifications
url: '#get-verifications'
structuredData:
headings:
- content: Get Key
id: get-key
- content: Delete Key
id: delete-key
- content: Create Key
id: create-key
- content: Verify Key
id: verify-key
- content: Update Key
id: update-key
- content: Update Remaining
id: update-remaining
- content: Get Verifications
id: get-verifications
contents: []
---
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/keys.getKey"},{"method":"post","path":"/v1/keys.deleteKey"},{"method":"post","path":"/v1/keys.createKey"},{"method":"post","path":"/v1/keys.verifyKey"},{"method":"post","path":"/v1/keys.updateKey"},{"method":"post","path":"/v1/keys.updateRemaining"},{"method":"get","path":"/v1/keys.getVerifications"}]} hasHead={true} />
@@ -1,16 +0,0 @@
---
title: Liveness
full: true
_openapi:
toc:
- depth: 2
title: Check Liveness
url: '#check-liveness'
structuredData:
headings:
- content: Check Liveness
id: check-liveness
contents: []
---
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/liveness"}]} hasHead={true} />
File diff suppressed because one or more lines are too long
@@ -1,615 +0,0 @@
---
title: ratelimits
full: true
---
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request, APIPlayground } from "fumadocs-ui/components/api";
<Root baseUrl={"https://api.unkey.dev"}>
<API>
<APIInfo method={"POST"} route={"/v1/ratelimits.limit"}>
## limit
### Authorization
<Property name={"Authorization"} type={"Bearer <token>"} required={true}>
In: `header`
</Property>
### Request Body
<Property name={"namespace"} type={"string"} required={false} deprecated={undefined}>
Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes.
<span>Example: `"email.outbound"`</span>
<span>Default: `"default"`</span>
</Property>
<Property name={"identifier"} type={"string"} required={true} deprecated={undefined}>
Identifier of your user, this can be their userId, an email, an ip or anything else.
<span>Example: `"user_123"`</span>
</Property>
<Property name={"limit"} type={"integer"} required={true} deprecated={undefined}>
How many requests may pass in a given window.
<span>Example: `10`</span>
<span>Minimum: `0`</span>
</Property>
<Property name={"duration"} type={"integer"} required={true} deprecated={undefined}>
The window duration in milliseconds
<span>Example: `60000`</span>
<span>Minimum: `1000`</span>
</Property>
<Property name={"cost"} type={"integer"} required={false} deprecated={undefined}>
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.
If there are not enough tokens left, the request is denied.
Set it to 0 to receive the current limit without changing anything.
<span>Example: `2`</span>
<span>Default: `1`</span>
<span>Minimum: `0`</span>
</Property>
<Property name={"async"} type={"boolean"} required={false} deprecated={undefined}>
Async will return a response immediately, lowering latency at the cost of accuracy.
<span>Default: `false`</span>
</Property>
<Property name={"meta"} type={"object"} required={false} deprecated={undefined}>
Attach any metadata to this request
<ObjectCollapsible name={"meta"}>
<Property name={"[key: string]"} type={"Any properties in string, boolean, number, null, null"} required={false} deprecated={undefined}>
</Property>
</ObjectCollapsible>
</Property>
<Property name={"resources"} type={"array<object>"} required={false} deprecated={undefined}>
Resources that are about to be accessed by the user
<span>Example: `[{"type":"project","id":"p_123","name":"dub"}]`</span>
<ObjectCollapsible name={"Object 1"}>
<Property name={"type"} type={"string"} required={true} deprecated={undefined}>
The type of resource
<span>Example: `"organization"`</span>
</Property>
<Property name={"id"} type={"string"} required={true} deprecated={undefined}>
The unique identifier for the resource
<span>Example: `"org_123"`</span>
</Property>
<Property name={"name"} type={"string"} required={false} deprecated={undefined}>
A human readable name for this resource
<span>Example: `"unkey"`</span>
</Property>
<Property name={"meta"} type={"object"} required={false} deprecated={undefined}>
Attach any metadata to this resources
<ObjectCollapsible name={"meta"}>
<Property name={"[key: string]"} type={"Any properties in string, boolean, number, null, null"} required={false} deprecated={undefined}>
</Property>
</ObjectCollapsible>
</Property>
</ObjectCollapsible>
</Property>
| Status code | Description |
| ----------- | ----------- |
| `200` | |
| `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 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>
<Requests items={["cURL","JavaScript"]}>
<Request value={"cURL"}>
```bash
curl -X POST "https://api.unkey.dev/v1/ratelimits.limit" \
-d '{
"namespace": "email.outbound",
"identifier": "user_123",
"limit": 10,
"duration": 60000,
"cost": 2,
"async": false,
"meta": {
"property1": "string",
"property2": "string"
},
"resources": [
{
"type": "project",
"id": "p_123",
"name": "dub"
}
]
}'
```
</Request>
<Request value={"JavaScript"}>
```js
fetch("https://api.unkey.dev/v1/ratelimits.limit", {
method: "POST"
});
```
</Request>
</Requests>
<Responses items={["200","400","401","403","404","409","429","500"]}>
<Response value={"200"}>
<ResponseTypes>
<ExampleResponse>
```json
{
"success": true,
"limit": 10,
"remaining": 9,
"reset": 1709804263654
}
```
</ExampleResponse>
<TypeScriptResponse>
```ts
export interface Response {
/**
* Returns true if the request should be processed, false if it was rejected.
*/
success: boolean;
/**
* How many requests are allowed within a window.
*/
limit: number;
/**
* How many requests can still be made in the current window.
*/
remaining: number;
/**
* A unix millisecond timestamp when the limits reset.
*/
reset: number;
}
```
</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>
</Root>
+3
View File
@@ -1546,6 +1546,9 @@ importers:
github-slugger:
specifier: ^2.0.0
version: 2.0.0
gray-matter:
specifier: ^4.0.3
version: 4.0.3
hast-util-to-jsx-runtime:
specifier: ^2.3.6
version: 2.3.6