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: .
|
context: .
|
||||||
dockerfile: packages/server/Dockerfile
|
dockerfile: packages/server/Dockerfile
|
||||||
target: deps
|
target: deps
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
npx prisma generate --schema=prisma/schema.prisma &&
|
npx prisma generate --schema=prisma/schema.prisma &&
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
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 OpenApiDoc = OpenAPIV3.Document | OpenAPIV3_1.Document;
|
||||||
|
type SwaggerDoc = OpenAPIV2.Document;
|
||||||
|
|
||||||
export type ParsedModule = {
|
export type ParsedModule = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -34,27 +35,55 @@ export type ParseResult = {
|
|||||||
endpoints: ParsedEndpoint[];
|
endpoints: ParsedEndpoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function parseOpenApiDocument(input: string | object): Promise<ParseResult> {
|
function isSwagger2(api: OpenAPI.Document): api is SwaggerDoc {
|
||||||
const rawApi = await SwaggerParser.validate(input as any);
|
return 'swagger' in api && (api as any).swagger?.startsWith('2.');
|
||||||
const api = await SwaggerParser.dereference(rawApi as any) as OpenApiDoc;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const tagMap = new Map<string, string | null>();
|
|
||||||
if (api.tags) {
|
|
||||||
for (const tag of api.tags) {
|
|
||||||
tagMap.set(tag.name, tag.description || 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 endpoints: ParsedEndpoint[] = [];
|
||||||
const usedTags = new Set<string>();
|
|
||||||
|
|
||||||
const paths = api.paths || {};
|
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;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
||||||
if (!pathItem) continue;
|
if (!pathItem) continue;
|
||||||
const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as const;
|
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;
|
if (!operation) continue;
|
||||||
|
|
||||||
const endpointTags = operation.tags || [];
|
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 prefix = pathStr.split('/').filter(Boolean)[0] || 'default';
|
||||||
const moduleName = endpointTags[0] || prefix;
|
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 modules: ParsedModule[] = [];
|
||||||
const moduleNames = new Set<string>();
|
const moduleNames = new Set<string>();
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,20 @@ export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
let body: Record<string, unknown>;
|
let spec: unknown;
|
||||||
|
|
||||||
if (mode === 'url') {
|
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 {
|
} 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', {
|
const data = await apiFetch<ImportResult>('/projects', {
|
||||||
method: 'POST', body: JSON.stringify(body),
|
method: 'POST', body: JSON.stringify({ spec }),
|
||||||
});
|
});
|
||||||
setResult(data);
|
setResult(data);
|
||||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||||
|
|||||||
Reference in New Issue
Block a user