Compare commits

..

4 Commits

Author SHA1 Message Date
Greg Bergé 404c8df073 Upgrade API client 2025-02-21 14:22:27 +01:00
Nolann B. 05e1d8cd96 Hide x-gitbook-* symbols in OpenAPI blocks (#2863) 2025-02-21 12:38:55 +01:00
Greg Bergé 9f0de74caa Add support for new OpenAPI ref (#2860) 2025-02-20 14:31:04 +01:00
spastorelli a820739bd2 Remove ununsed search api lib methods (#2855) 2025-02-20 09:46:12 +01:00
14 changed files with 162 additions and 75 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Remove unused search api method from gitbook/api/lib
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Hide x-gitbook-\* symbols in OpenAPI blocks
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add support for new OpenAPI ref
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix ID not set when there is no operation summary
+7 -5
View File
@@ -24,7 +24,7 @@
}, },
"packages/colors": { "packages/colors": {
"name": "@gitbook/colors", "name": "@gitbook/colors",
"version": "0.1.0", "version": "0.2.0",
"devDependencies": { "devDependencies": {
"typescript": "^5.5.3", "typescript": "^5.5.3",
}, },
@@ -38,9 +38,9 @@
}, },
"packages/gitbook": { "packages/gitbook": {
"name": "gitbook", "name": "gitbook",
"version": "0.6.1", "version": "0.6.2",
"dependencies": { "dependencies": {
"@gitbook/api": "^0.93.0", "@gitbook/api": "^0.95.0",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*",
@@ -155,7 +155,7 @@
}, },
"packages/openapi-parser": { "packages/openapi-parser": {
"name": "@gitbook/openapi-parser", "name": "@gitbook/openapi-parser",
"version": "1.0.0", "version": "1.0.1",
"dependencies": { "dependencies": {
"@scalar/openapi-parser": "^0.10.4", "@scalar/openapi-parser": "^0.10.4",
"@scalar/openapi-types": "^0.1.6", "@scalar/openapi-types": "^0.1.6",
@@ -211,7 +211,7 @@
}, },
"packages/react-openapi": { "packages/react-openapi": {
"name": "@gitbook/react-openapi", "name": "@gitbook/react-openapi",
"version": "1.0.1", "version": "1.0.2",
"dependencies": { "dependencies": {
"@gitbook/openapi-parser": "workspace:*", "@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "1.0.87", "@scalar/api-client-react": "1.0.87",
@@ -4651,6 +4651,8 @@
"gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"gitbook/@gitbook/api": ["@gitbook/api@0.95.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9KAbt27Ile6cqAch7QEbiJHALQHojYlhsPzilgdQ5wpHgLwsrd7Smd58A3/8bWBKq4KV0vP4rh3oYhIw+LlWFw=="],
"gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="], "gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="],
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="], "glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
+1 -1
View File
@@ -17,7 +17,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
}, },
"dependencies": { "dependencies": {
"@gitbook/api": "^0.93.0", "@gitbook/api": "^0.95.0",
"@gitbook/cache-do": "workspace:*", "@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*",
@@ -3,7 +3,7 @@ import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi'; import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react'; import React from 'react';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch'; import { resolveOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block'; import { BlockProps } from '../Block';
@@ -27,13 +27,17 @@ export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) { async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
const { block, context } = props; const { block, context } = props;
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
const { data, specUrl, error } = await resolveOpenAPIBlock({
block,
context: { resolveContentRef: context.resolveContentRef },
});
if (error) { if (error) {
return ( return (
<div className={tcls('hidden')}> <div className="hidden">
<p> <p>
Error with {error.rootURL}: {error.message} Error with {specUrl}: {error.message}
</p> </p>
</div> </div>
); );
+44 -49
View File
@@ -208,6 +208,42 @@ export const getUserById = cache({
}, },
}); });
/**
* Get the latest version of an OpenAPI spec by its slug.
*/
export const getLatestOpenAPISpecVersionContent = cache({
name: 'api.getLatestOpenApiSpecVersionContent',
tag: (organization, openAPISpec) =>
getAPICacheTag({
tag: 'openapi',
organization,
openAPISpec,
}),
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
try {
const apiCtx = await api();
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersionContent(
organizationId,
slug,
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, { revalidateBefore: 60 * 60 });
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return {
revalidateBefore: 5,
data: null,
};
}
throw error;
}
},
});
/** /**
* Resolve a URL to the content to render. * Resolve a URL to the content to render.
*/ */
@@ -1066,55 +1102,6 @@ export async function getSpaceContentData(
}; };
} }
/**
* Search content in a space.
*/
export const searchSpaceContent = cache({
name: 'api.searchSpaceContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (
spaceId: string,
/** The revision ID is used as a cache bust key, to avoid revalidating lot of cache entries by tags */
revisionId: string,
query: string,
options: CacheFunctionOptions,
) => {
const apiCtx = await api();
const response = await apiCtx.client.spaces.searchSpaceContent(
spaceId,
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response);
},
});
/**
* Search content accross all spaces in a parent (site or collection).
*/
export const searchParentContent = cache({
name: 'api.searchParentContent',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
getKeySuffix: getAPIContextId,
get: async (parentId: string, query: string, options: CacheFunctionOptions) => {
const apiCtx = await api();
const response = await apiCtx.client.search.searchContent(
{ query },
{
...noCacheFetchOptions,
signal: options.signal,
},
);
return cacheResponse(response, {
ttl: 60 * 60,
});
},
});
/** /**
* Search content in a Site or specific SiteSpaces. * Search content in a Site or specific SiteSpaces.
*/ */
@@ -1275,6 +1262,12 @@ export function getAPICacheTag(
| { | {
tag: 'site'; tag: 'site';
site: string; site: string;
}
// All data related to an OpenAPI spec
| {
tag: 'openapi';
organization: string;
openAPISpec: string;
}, },
): string { ): string {
switch (spec.tag) { switch (spec.tag) {
@@ -1298,6 +1291,8 @@ export function getAPICacheTag(
return `site:${spec.site}`; return `site:${spec.site}`;
case 'integration': case 'integration':
return `integration:${spec.integration}`; return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
default: default:
assertNever(spec); assertNever(spec);
} }
@@ -1,7 +1,7 @@
import { JSONDocument, ContentRef } from '@gitbook/api'; import { JSONDocument, ContentRef } from '@gitbook/api';
import { getNodeText } from './document'; import { getNodeText } from './document';
import { fetchOpenAPIBlock } from './openapi/fetch'; import { resolveOpenAPIBlock } from './openapi/fetch';
import { ResolvedContentRef } from './references'; import { ResolvedContentRef } from './references';
export interface DocumentSection { export interface DocumentSection {
@@ -38,7 +38,10 @@ export async function getDocumentSections(
} }
if (block.type === 'swagger' && block.meta?.id) { if (block.type === 'swagger' && block.meta?.id) {
const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef); const { data: operation } = await resolveOpenAPIBlock({
block,
context: { resolveContentRef },
});
if (operation) { if (operation) {
sections.push({ sections.push({
id: block.meta.id, id: block.meta.id,
+35 -10
View File
@@ -7,23 +7,48 @@ import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
import { enrichFilesystem } from './enrich'; import { enrichFilesystem } from './enrich';
import { ResolvedContentRef } from '../references'; import { ResolvedContentRef } from '../references';
const weakmap = new WeakMap<DocumentBlockOpenAPI, ResolveOpenAPIBlockResult>();
/** /**
* Fetch an OpenAPI specification for an operation. * Cache the result of resolving an OpenAPI block.
* It is important because the resolve is called in sections and in the block itself.
*/ */
export async function fetchOpenAPIBlock( export function resolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
block: DocumentBlockOpenAPI, if (weakmap.has(args.block)) {
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>, return weakmap.get(args.block)!;
): Promise< }
| { data: OpenAPIOperationData | null; specUrl: string | null; error?: undefined }
const result = baseResolveOpenAPIBlock(args);
weakmap.set(args.block, result);
return result;
}
type ResolveOpenAPIBlockArgs = {
block: DocumentBlockOpenAPI;
context: { resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null> };
};
type ResolveOpenAPIBlockResult = Promise<
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined } | { error: OpenAPIParseError; data?: undefined; specUrl?: undefined }
> { >;
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null; /**
if (!resolved || !block.data.path || !block.data.method) { * Resolve OpenAPI block.
*/
async function baseResolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
const { context, block } = args;
if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null };
}
const resolved = block.data.ref ? await context.resolveContentRef(block.data.ref) : null;
if (!resolved) {
return { data: null, specUrl: null }; return { data: null, specUrl: null };
} }
try { try {
const filesystem = await fetchFilesystem(resolved.href); const filesystem = resolved.openAPIFilesystem ?? (await fetchFilesystem(resolved.href));
const data = await resolveOpenAPIOperation(filesystem, { const data = await resolveOpenAPIOperation(filesystem, {
path: block.data.path, path: block.data.path,
method: block.data.method, method: block.data.method,
+26
View File
@@ -7,6 +7,7 @@ import {
SiteSpace, SiteSpace,
Space, Space,
} from '@gitbook/api'; } from '@gitbook/api';
import type { Filesystem } from '@gitbook/openapi-parser';
import assertNever from 'assert-never'; import assertNever from 'assert-never';
import React from 'react'; import React from 'react';
@@ -17,6 +18,8 @@ import {
SpaceContentPointer, SpaceContentPointer,
getCollection, getCollection,
getDocument, getDocument,
getLatestOpenAPISpecVersion,
getLatestOpenAPISpecVersionContent,
getPageDocument, getPageDocument,
getPublishedContentSite, getPublishedContentSite,
getReusableContent, getReusableContent,
@@ -50,6 +53,8 @@ export interface ResolvedContentRef {
file?: RevisionFile; file?: RevisionFile;
/** Resolved reusable content, if the ref points to reusable content on a revision. */ /** Resolved reusable content, if the ref points to reusable content on a revision. */
reusableContent?: RevisionReusableContent; reusableContent?: RevisionReusableContent;
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
} }
export interface ContentRefContext extends PageHrefContext { export interface ContentRefContext extends PageHrefContext {
@@ -272,6 +277,27 @@ export async function resolveContentRef(
}; };
} }
case 'openapi': {
if (!siteContext) {
return null;
}
const { organizationId } = siteContext;
const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
organizationId,
contentRef.spec,
);
if (!openAPISpecVersionContent) {
return null;
}
return {
href: openAPISpecVersionContent.url,
text: contentRef.spec,
active: false,
openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem,
};
}
default: default:
assertNever(contentRef); assertNever(contentRef);
} }
@@ -29,7 +29,7 @@ export function OpenAPIOperation(props: {
return ( return (
<div className={clsx('openapi-operation', className)}> <div className={clsx('openapi-operation', className)}>
<div className="openapi-summary"> <div className="openapi-summary" id={operation.summary ? undefined : context.id}>
{operation.summary {operation.summary
? context.renderHeading({ ? context.renderHeading({
deprecated: operation.deprecated ?? false, deprecated: operation.deprecated ?? false,
@@ -5,6 +5,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { InteractiveSection } from './InteractiveSection'; import { InteractiveSection } from './InteractiveSection';
import { json2xml } from './json2xml'; import { json2xml } from './json2xml';
import { stringifyOpenAPI } from './stringifyOpenAPI';
/** /**
* Display an example of the response content. * Display an example of the response content.
@@ -226,7 +227,7 @@ function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean
return json2xml(example.value); return json2xml(example.value);
} }
return JSON.stringify(example.value, null, 2); return stringifyOpenAPI(example.value, null, 2);
} }
/** /**
+13 -2
View File
@@ -1,6 +1,17 @@
/** /**
* Stringify an OpenAPI object. Same API as JSON.stringify. * Stringify an OpenAPI object. Same API as JSON.stringify.
*/ */
export function stringifyOpenAPI(body: unknown, transformer?: null, indent?: number): string { export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string {
return JSON.stringify(body, transformer, indent); return JSON.stringify(
body,
(key, value) => {
// Ignore internal keys
if (key.startsWith('x-gitbook-')) {
return undefined;
}
return value;
},
indent,
);
} }