fix: fetch OpenAPI doc from browser to avoid Docker network isolation, add Swagger 2.0 support
This commit is contained in:
@@ -8,6 +8,8 @@ services:
|
||||
context: .
|
||||
dockerfile: packages/server/Dockerfile
|
||||
target: deps
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
command: >
|
||||
sh -c "
|
||||
npx prisma generate --schema=prisma/schema.prisma &&
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
import type { OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
|
||||
type OpenApiDoc = OpenAPIV3.Document | OpenAPIV3_1.Document;
|
||||
type SwaggerDoc = OpenAPIV2.Document;
|
||||
|
||||
export type ParsedModule = {
|
||||
name: string;
|
||||
@@ -34,27 +35,55 @@ export type ParseResult = {
|
||||
endpoints: ParsedEndpoint[];
|
||||
};
|
||||
|
||||
export async function parseOpenApiDocument(input: string | object): Promise<ParseResult> {
|
||||
const rawApi = await SwaggerParser.validate(input as any);
|
||||
const api = await SwaggerParser.dereference(rawApi as any) as OpenApiDoc;
|
||||
function isSwagger2(api: OpenAPI.Document): api is SwaggerDoc {
|
||||
return 'swagger' in api && (api as any).swagger?.startsWith('2.');
|
||||
}
|
||||
|
||||
const openApiVersion = 'openapi' in api ? api.openapi : 'unknown';
|
||||
const name = api.info.title;
|
||||
const description = api.info.description || null;
|
||||
const version = api.info.version;
|
||||
const baseUrl = api.servers?.[0]?.url || null;
|
||||
function parseSwagger2Endpoints(api: SwaggerDoc): { endpoints: ParsedEndpoint[]; baseUrl: string | null } {
|
||||
const baseUrl = api.basePath || (api.host ? `http://${api.host}${api.basePath || ''}` : null);
|
||||
const endpoints: ParsedEndpoint[] = [];
|
||||
const paths = api.paths || {};
|
||||
|
||||
const tagMap = new Map<string, string | null>();
|
||||
if (api.tags) {
|
||||
for (const tag of api.tags) {
|
||||
tagMap.set(tag.name, tag.description || null);
|
||||
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
||||
if (!pathItem) continue;
|
||||
const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as const;
|
||||
for (const method of methods) {
|
||||
const operation = (pathItem as Record<string, unknown>)[method] as OpenAPIV2.OperationObject | undefined;
|
||||
if (!operation) continue;
|
||||
|
||||
const endpointTags = operation.tags || [];
|
||||
const prefix = pathStr.split('/').filter(Boolean)[0] || 'default';
|
||||
const moduleName = endpointTags[0] || prefix;
|
||||
|
||||
// Convert Swagger 2 body parameter to requestBody-like structure
|
||||
const params = (operation.parameters || []) as OpenAPIV2.Parameter[];
|
||||
const bodyParam = params.find((p: any) => p.in === 'body');
|
||||
const nonBodyParams = params.filter((p: any) => p.in !== 'body');
|
||||
|
||||
endpoints.push({
|
||||
method: method.toUpperCase(),
|
||||
path: pathStr,
|
||||
summary: operation.summary || null,
|
||||
description: operation.description || null,
|
||||
operationId: operation.operationId || null,
|
||||
parameters: nonBodyParams as unknown[],
|
||||
requestBody: bodyParam ? { schema: (bodyParam as any).schema } : null,
|
||||
responses: (operation.responses || {}) as Record<string, unknown>,
|
||||
tags: endpointTags,
|
||||
deprecated: operation.deprecated || false,
|
||||
moduleName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const endpoints: ParsedEndpoint[] = [];
|
||||
const usedTags = new Set<string>();
|
||||
return { endpoints, baseUrl };
|
||||
}
|
||||
|
||||
function parseOpenApi3Endpoints(api: OpenApiDoc): { endpoints: ParsedEndpoint[]; baseUrl: string | null } {
|
||||
const baseUrl = api.servers?.[0]?.url || null;
|
||||
const endpoints: ParsedEndpoint[] = [];
|
||||
const paths = api.paths || {};
|
||||
|
||||
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
||||
if (!pathItem) continue;
|
||||
const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as const;
|
||||
@@ -63,11 +92,6 @@ export async function parseOpenApiDocument(input: string | object): Promise<Pars
|
||||
if (!operation) continue;
|
||||
|
||||
const endpointTags = operation.tags || [];
|
||||
for (const tag of endpointTags) {
|
||||
usedTags.add(tag);
|
||||
if (!tagMap.has(tag)) tagMap.set(tag, null);
|
||||
}
|
||||
|
||||
const prefix = pathStr.split('/').filter(Boolean)[0] || 'default';
|
||||
const moduleName = endpointTags[0] || prefix;
|
||||
|
||||
@@ -87,6 +111,44 @@ export async function parseOpenApiDocument(input: string | object): Promise<Pars
|
||||
}
|
||||
}
|
||||
|
||||
return { endpoints, baseUrl };
|
||||
}
|
||||
|
||||
export async function parseOpenApiDocument(input: string | object): Promise<ParseResult> {
|
||||
// Parse and dereference all $refs inline
|
||||
const api = await SwaggerParser.dereference(input as any, {
|
||||
dereference: { circular: 'ignore' },
|
||||
}) as OpenAPI.Document;
|
||||
|
||||
const isV2 = isSwagger2(api);
|
||||
const openApiVersion = isV2 ? (api as SwaggerDoc).swagger : ('openapi' in api ? (api as any).openapi : 'unknown');
|
||||
const name = api.info.title;
|
||||
const description = api.info.description || null;
|
||||
const version = api.info.version;
|
||||
|
||||
// Collect tags
|
||||
const tagMap = new Map<string, string | null>();
|
||||
if (api.tags) {
|
||||
for (const tag of api.tags) {
|
||||
tagMap.set(tag.name, tag.description || null);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse endpoints based on spec version
|
||||
const { endpoints, baseUrl } = isV2
|
||||
? parseSwagger2Endpoints(api as SwaggerDoc)
|
||||
: parseOpenApi3Endpoints(api as OpenApiDoc);
|
||||
|
||||
// Track used tags
|
||||
const usedTags = new Set<string>();
|
||||
for (const ep of endpoints) {
|
||||
for (const tag of ep.tags) {
|
||||
usedTags.add(tag);
|
||||
if (!tagMap.has(tag)) tagMap.set(tag, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Build modules
|
||||
const modules: ParsedModule[] = [];
|
||||
const moduleNames = new Set<string>();
|
||||
|
||||
|
||||
@@ -31,14 +31,20 @@ export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
let body: Record<string, unknown>;
|
||||
let spec: unknown;
|
||||
|
||||
if (mode === 'url') {
|
||||
body = { specUrl: url };
|
||||
// Fetch from browser (can access local network) instead of letting server fetch
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);
|
||||
const text = await res.text();
|
||||
try { spec = JSON.parse(text); } catch { spec = text; }
|
||||
} else {
|
||||
try { body = { spec: JSON.parse(fileContent) }; } catch { body = { spec: fileContent }; }
|
||||
try { spec = JSON.parse(fileContent); } catch { spec = fileContent; }
|
||||
}
|
||||
|
||||
const data = await apiFetch<ImportResult>('/projects', {
|
||||
method: 'POST', body: JSON.stringify(body),
|
||||
method: 'POST', body: JSON.stringify({ spec }),
|
||||
});
|
||||
setResult(data);
|
||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
|
||||
Reference in New Issue
Block a user