mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Avoid dereferencing before caching (#2793)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Do not dereference before caching OpenAPI spec.
|
||||
@@ -49,7 +49,7 @@ export async function fetchOpenAPIBlock(
|
||||
|
||||
const fetcher: OpenAPIFetcher = {
|
||||
fetch: cache({
|
||||
name: 'openapi.fetch.v3',
|
||||
name: 'openapi.fetch.v4',
|
||||
get: async (url: string, options: CacheFunctionOptions) => {
|
||||
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
||||
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toJSON, fromJSON } from 'flatted';
|
||||
|
||||
import { OpenAPICustomSpecProperties } from './parser';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
|
||||
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { noReference } from './utils';
|
||||
import { parseDescriptions } from './parser/markdown';
|
||||
import { dereference } from '@scalar/openapi-parser';
|
||||
|
||||
export interface OpenAPIFetcher {
|
||||
/**
|
||||
@@ -44,7 +44,8 @@ export async function fetchOpenAPIOperation(
|
||||
},
|
||||
fetcher: OpenAPIFetcher,
|
||||
): Promise<OpenAPIOperationData | null> {
|
||||
const schema = await fetcher.fetch(input.url);
|
||||
const refSchema = await fetcher.fetch(input.url);
|
||||
const schema = await memoDereferenceSchema(refSchema, input.url);
|
||||
|
||||
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
||||
|
||||
@@ -89,6 +90,38 @@ export async function fetchOpenAPIOperation(
|
||||
};
|
||||
}
|
||||
|
||||
const dereferenceSchemaCache = new WeakMap<OpenAPI.Document, Promise<OpenAPI.Document>>();
|
||||
|
||||
/**
|
||||
* Memoized version of `dereferenceSchema`.
|
||||
*/
|
||||
function memoDereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
||||
if (dereferenceSchemaCache.has(schema)) {
|
||||
return dereferenceSchemaCache.get(schema) as Promise<T>;
|
||||
}
|
||||
|
||||
const promise = dereferenceSchema(schema, url);
|
||||
dereferenceSchemaCache.set(schema, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereference an OpenAPI schema.
|
||||
*/
|
||||
async function dereferenceSchema<T extends OpenAPI.Document>(schema: T, url: string): Promise<T> {
|
||||
const derefResult = await dereference(schema);
|
||||
|
||||
if (!derefResult.schema) {
|
||||
throw new OpenAPIParseError(
|
||||
'Failed to dereference OpenAPI document',
|
||||
url,
|
||||
'failed-dereference',
|
||||
);
|
||||
}
|
||||
|
||||
return derefResult.schema as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a path object from its path.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ export class OpenAPIParseError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly url: string,
|
||||
public readonly code?: 'invalid-spec' | 'v2-spec',
|
||||
public readonly code?: 'invalid-spec' | 'v2-spec' | 'failed-dereference',
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
import { expect, it } from 'bun:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { parseOpenAPI } from '.';
|
||||
|
||||
it('should parse and not give a recursive structure', async () => {
|
||||
const schema = await parseOpenAPI({
|
||||
value: await readFile(new URL('./fixtures/spec-example.json', import.meta.url), 'utf-8'),
|
||||
url: 'https://example.com',
|
||||
parseMarkdown: async (input) => input,
|
||||
});
|
||||
|
||||
JSON.stringify(schema);
|
||||
expect(schema.openapi).toBe('3.0.0');
|
||||
});
|
||||
@@ -15,8 +15,6 @@ export function traverse(
|
||||
return specification;
|
||||
}
|
||||
|
||||
seen.add(specification);
|
||||
|
||||
for (const [key, value] of Object.entries(specification)) {
|
||||
const currentPath = [...path, key];
|
||||
if (Array.isArray(value)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAPICustomSpecProperties } from './types';
|
||||
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
|
||||
import { AnyApiDefinitionFormat, validate } from '@scalar/openapi-parser';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIParseError } from './error';
|
||||
import { parseDescriptions } from './markdown';
|
||||
@@ -18,10 +18,10 @@ export async function parseOpenAPIV3(input: {
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
> {
|
||||
const { value, url } = input;
|
||||
const result = await dereference(value);
|
||||
const result = await validate(value);
|
||||
|
||||
// Spec is invalid, we stop here.
|
||||
if (!result.schema) {
|
||||
if (!result.specification) {
|
||||
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ export async function parseOpenAPIV3(input: {
|
||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||
}
|
||||
|
||||
const schema = await parseDescriptions({
|
||||
specification: result.schema,
|
||||
const specification = await parseDescriptions({
|
||||
specification: result.specification,
|
||||
parseMarkdown: input.parseMarkdown,
|
||||
});
|
||||
|
||||
switch (result.version) {
|
||||
case '3.0':
|
||||
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
return specification as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
case '3.1':
|
||||
default:
|
||||
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||
return specification as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user