mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +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 = {
|
const fetcher: OpenAPIFetcher = {
|
||||||
fetch: cache({
|
fetch: cache({
|
||||||
name: 'openapi.fetch.v3',
|
name: 'openapi.fetch.v4',
|
||||||
get: async (url: string, options: CacheFunctionOptions) => {
|
get: async (url: string, options: CacheFunctionOptions) => {
|
||||||
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
// 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:
|
// 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 { toJSON, fromJSON } from 'flatted';
|
||||||
|
|
||||||
import { OpenAPICustomSpecProperties } from './parser';
|
import { OpenAPICustomSpecProperties, OpenAPIParseError } from './parser';
|
||||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
import { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||||
import { noReference } from './utils';
|
import { noReference } from './utils';
|
||||||
import { parseDescriptions } from './parser/markdown';
|
import { dereference } from '@scalar/openapi-parser';
|
||||||
|
|
||||||
export interface OpenAPIFetcher {
|
export interface OpenAPIFetcher {
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,8 @@ export async function fetchOpenAPIOperation(
|
|||||||
},
|
},
|
||||||
fetcher: OpenAPIFetcher,
|
fetcher: OpenAPIFetcher,
|
||||||
): Promise<OpenAPIOperationData | null> {
|
): 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);
|
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.
|
* Get a path object from its path.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export class OpenAPIParseError extends Error {
|
|||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public readonly url: string,
|
public readonly url: string,
|
||||||
public readonly code?: 'invalid-spec' | 'v2-spec',
|
public readonly code?: 'invalid-spec' | 'v2-spec' | 'failed-dereference',
|
||||||
) {
|
) {
|
||||||
super(message);
|
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;
|
return specification;
|
||||||
}
|
}
|
||||||
|
|
||||||
seen.add(specification);
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(specification)) {
|
for (const [key, value] of Object.entries(specification)) {
|
||||||
const currentPath = [...path, key];
|
const currentPath = [...path, key];
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OpenAPICustomSpecProperties } from './types';
|
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 { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||||
import { OpenAPIParseError } from './error';
|
import { OpenAPIParseError } from './error';
|
||||||
import { parseDescriptions } from './markdown';
|
import { parseDescriptions } from './markdown';
|
||||||
@@ -18,10 +18,10 @@ export async function parseOpenAPIV3(input: {
|
|||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||||
> {
|
> {
|
||||||
const { value, url } = input;
|
const { value, url } = input;
|
||||||
const result = await dereference(value);
|
const result = await validate(value);
|
||||||
|
|
||||||
// Spec is invalid, we stop here.
|
// Spec is invalid, we stop here.
|
||||||
if (!result.schema) {
|
if (!result.specification) {
|
||||||
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
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');
|
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = await parseDescriptions({
|
const specification = await parseDescriptions({
|
||||||
specification: result.schema,
|
specification: result.specification,
|
||||||
parseMarkdown: input.parseMarkdown,
|
parseMarkdown: input.parseMarkdown,
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (result.version) {
|
switch (result.version) {
|
||||||
case '3.0':
|
case '3.0':
|
||||||
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
return specification as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||||
case '3.1':
|
case '3.1':
|
||||||
default:
|
default:
|
||||||
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
return specification as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user