feat: add project CRUD, OpenAPI import/parsing, module and endpoint management routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:48:06 +08:00
parent 896115a438
commit a191a4db00
8 changed files with 548 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
type OpenApiDoc = OpenAPIV3.Document | OpenAPIV3_1.Document;
export type ParsedModule = {
name: string;
description: string | null;
source: 'tag' | 'path_prefix';
};
export type ParsedEndpoint = {
method: string;
path: string;
summary: string | null;
description: string | null;
operationId: string | null;
parameters: unknown[];
requestBody: unknown | null;
responses: Record<string, unknown>;
tags: string[];
deprecated: boolean;
moduleName: string;
};
export type ParseResult = {
name: string;
description: string | null;
version: string;
openApiVersion: string;
baseUrl: string | null;
spec: unknown;
modules: ParsedModule[];
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;
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);
}
}
const endpoints: ParsedEndpoint[] = [];
const usedTags = new Set<string>();
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 OpenAPIV3.OperationObject | undefined;
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;
endpoints.push({
method: method.toUpperCase(),
path: pathStr,
summary: operation.summary || null,
description: operation.description || null,
operationId: operation.operationId || null,
parameters: (operation.parameters || []) as unknown[],
requestBody: operation.requestBody || null,
responses: (operation.responses || {}) as Record<string, unknown>,
tags: endpointTags,
deprecated: operation.deprecated || false,
moduleName,
});
}
}
const modules: ParsedModule[] = [];
const moduleNames = new Set<string>();
for (const [tagName, tagDesc] of tagMap) {
if (usedTags.has(tagName)) {
modules.push({ name: tagName, description: tagDesc, source: 'tag' });
moduleNames.add(tagName);
}
}
for (const endpoint of endpoints) {
if (endpoint.tags.length === 0 && !moduleNames.has(endpoint.moduleName)) {
modules.push({ name: endpoint.moduleName, description: null, source: 'path_prefix' });
moduleNames.add(endpoint.moduleName);
}
}
return { name, description, version, openApiVersion, baseUrl, spec: api, modules, endpoints };
}