UNPKG

go-meow

Version:

A modular microservice template built with TypeScript, Express, and Prisma (MongoDB). It includes service scaffolding tools, consistent query utilities with data grouping, Zod validation, structured logging, comprehensive seeding system, and Swagger/OpenA

218 lines (198 loc) 6.53 kB
import fs from "fs"; import path from "path"; interface OpenAPISpec { paths?: Record<string, any>; } interface SimplifiedEndpoint { id: string; method: string; url: string; summary?: string; description?: string; tags?: string[]; deprecated?: boolean; security?: any; contentTypes?: string[]; pathParams?: Array<{ name: string; required?: boolean; schema?: any; description?: string; }>; queryParams?: Array<{ name: string; required?: boolean; schema?: any; description?: string; }>; requestBody?: { required?: boolean; schema?: any; example?: any; contentTypes?: string[]; }; responses?: Record< string, { description?: string; schema?: any; example?: any; contentTypes?: string[]; } >; paginationHints?: { hasPaginationParams: boolean; hasSearchParam: boolean; hasSortParams: boolean; }; } function extractRequestBody(requestBody: any): SimplifiedEndpoint["requestBody"] | undefined { if (!requestBody || !requestBody.content) return undefined; const contentTypes = Object.keys(requestBody.content || {}); const json = requestBody.content["application/json"] || requestBody.content[contentTypes[0]]; if (!json) { return { required: requestBody.required, contentTypes }; } let example: any | undefined = undefined; if (json.example) example = json.example; else if (json.examples) { const first = Object.values(json.examples)[0] as any; if (first && (first as any).value) example = (first as any).value; } return { required: requestBody.required, schema: json.schema, example, contentTypes, }; } function extractResponses(responses: any): SimplifiedEndpoint["responses"] { const result: SimplifiedEndpoint["responses"] = {}; if (!responses || typeof responses !== "object") return result; for (const [status, resp] of Object.entries<any>(responses)) { const content = resp?.content || {}; const contentTypes = Object.keys(content); const json = content["application/json"] || content[contentTypes[0]] || {}; let example: any | undefined = undefined; if (json.example) example = json.example; else if (json.examples) { const first = Object.values(json.examples)[0] as any; if (first && (first as any).value) example = (first as any).value; } result[status] = { description: resp?.description, schema: json.schema, example, contentTypes, }; } return result; } function resolveInputPath(): string { const swaggerPath = path.resolve(process.cwd(), "docs/generated/swagger.json"); if (fs.existsSync(swaggerPath)) return swaggerPath; const openapiPath = path.resolve(process.cwd(), "docs/generated/openapi.json"); if (fs.existsSync(openapiPath)) return openapiPath; return swaggerPath; // default path for error reporting } function run() { const inputPath = resolveInputPath(); const outputDir = path.resolve(process.cwd(), "docs/generated"); const outputPath = path.join(outputDir, "endpoints.json"); if (!fs.existsSync(inputPath)) { console.error(`Spec file not found. Expected docs/generated/swagger.json or openapi.json`); process.exit(1); } const spec = JSON.parse(fs.readFileSync(inputPath, "utf8")) as OpenAPISpec; const result: SimplifiedEndpoint[] = []; const paths = spec.paths || {}; for (const [rawPath, methods] of Object.entries(paths)) { // skip non-path keys accidentally placed under paths (like security) if (!rawPath.startsWith("/")) continue; for (const [method, op] of Object.entries<any>(methods)) { const methodUpper = method.toUpperCase(); if ( !["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"].includes(methodUpper) ) { continue; } const opId = op.operationId || `${methodUpper} ${rawPath}`; const allParams: Array<any> = Array.isArray(op.parameters) ? op.parameters : []; const pathParams = allParams .filter((p) => p.in === "path") .map((p) => ({ name: p.name, required: p.required, schema: p.schema, description: p.description, })); const queryParams = allParams .filter((p) => p.in === "query") .map((p) => ({ name: p.name, required: p.required, schema: p.schema, description: p.description, })); const requestBody = extractRequestBody(op.requestBody); const responses = extractResponses(op.responses); const contentTypes = requestBody?.contentTypes || []; const ep: SimplifiedEndpoint = { id: opId, method: methodUpper, url: rawPath, summary: op.summary, description: op.description, tags: op.tags, deprecated: Boolean(op.deprecated), security: op.security, contentTypes, pathParams, queryParams, requestBody, responses, paginationHints: { hasPaginationParams: queryParams.some((p) => ["page", "limit"].includes((p.name || "").toLowerCase()), ), hasSearchParam: queryParams.some( (p) => (p.name || "").toLowerCase() === "query", ), hasSortParams: queryParams.some((p) => ["sort", "order"].includes((p.name || "").toLowerCase()), ), }, }; result.push(ep); } } if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(outputPath, JSON.stringify(result, null, 2)); // Also write per-tag endpoint files under docs/generated/endpoints/<tag>.endpoints.json const perTagDir = path.join(outputDir, "endpoints"); if (!fs.existsSync(perTagDir)) fs.mkdirSync(perTagDir, { recursive: true }); function toFileSlug(tag: string): string { return tag .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } const tagToEndpoints = new Map<string, SimplifiedEndpoint[]>(); for (const ep of result) { const tags = (ep.tags && ep.tags.length > 0 ? ep.tags : ["_untagged"]) as string[]; for (const tag of tags) { const key = toFileSlug(tag || "_untagged"); if (!tagToEndpoints.has(key)) tagToEndpoints.set(key, []); tagToEndpoints.get(key)!.push(ep); } } for (const [tagSlug, endpoints] of tagToEndpoints.entries()) { const tagFile = path.join(perTagDir, `${tagSlug}.endpoints.json`); fs.writeFileSync(tagFile, JSON.stringify(endpoints, null, 2)); } console.log( `Exported ${result.length} endpoints from ${path.basename(inputPath)} to ${outputPath} and ${perTagDir}`, ); } run();