UNPKG

postflame

Version:

🔥 Generate Postman collections automatically from Hono + Zod routes.

166 lines (165 loc) • 7.09 kB
import fs from 'fs'; import { parseZodSchema } from '../parser/zodParser.js'; export async function generatePostmanCollection(app, name = 'Hono API') { // Try to read OpenAPI JSON from the app's doc endpoint try { const res = await app.request?.('/doc'); if (res && res.ok) { const openapi = await res.json(); return openApiToPostman(openapi, name); } } catch { // Fallback to route-based below } // Fallback: route-based generation (no schemas) const routes = app.routes.map((route) => ({ method: route.method, path: route.path })); const dedup = new Map(); for (const r of routes) dedup.set(`${r.method} ${r.path}`, r); const collection = { info: { name, schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' }, item: Array.from(dedup.values()).map((r) => ({ name: `${r.method} ${r.path}`, request: { method: r.method, header: [], body: r.method === 'POST' || r.method === 'PUT' ? { mode: 'raw', raw: JSON.stringify(parseZodSchema(), null, 2) } : undefined, url: { raw: `{{baseUrl}}${r.path}`, host: ['{{baseUrl}}'], path: r.path.split('/').filter(Boolean), }, }, })), }; return collection; } function openApiToPostman(openapi, name) { const paths = openapi.paths || {}; const folders = new Map(); const ensureFolder = (tag) => { if (!folders.has(tag)) folders.set(tag, []); return folders.get(tag); }; for (const [rawPath, methods] of Object.entries(paths)) { for (const [method, op] of Object.entries(methods)) { const httpMethod = String(method).toUpperCase(); if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'].includes(httpMethod)) continue; const tagList = Array.isArray(op.tags) && op.tags.length ? op.tags : ['General']; // Handle path params: convert {id} -> :id for readability const pmPath = String(rawPath).replace(/\{(.*?)\}/g, ':$1'); const urlPath = pmPath.split('/').filter(Boolean); // Query params from both path-level and operation-level parameters const parameters = [...(methods.parameters || []), ...(op.parameters || [])]; const queryParams = parameters .filter((p) => p && p.in === 'query') .map((p) => ({ key: p.name, value: '', description: p.description })); // Build request body from supported content types let body = undefined; const content = op.requestBody?.content || {}; if (content['application/json']) { const json = content['application/json']; const example = json.example || json.examples?.default?.value; if (example) body = { mode: 'raw', raw: JSON.stringify(example, null, 2) }; else if (json.schema) body = { mode: 'raw', raw: JSON.stringify(json.schema, null, 2) }; } else if (content['multipart/form-data']) { const mp = content['multipart/form-data']; body = { mode: 'formdata', formdata: extractFormDataFromSchema(mp.schema) }; } else if (content['application/x-www-form-urlencoded']) { const urlenc = content['application/x-www-form-urlencoded']; body = { mode: 'urlencoded', urlencoded: extractUrlEncodedFromSchema(urlenc.schema) }; } // Saved responses from OpenAPI responses const responses = []; for (const [code, resp] of Object.entries(op.responses || {})) { const codeStr = String(code); const statusCode = /^\d{3}$/.test(codeStr) ? Number(codeStr) : 200; const json = resp?.content?.['application/json']; let bodyStr = ''; if (json?.example) bodyStr = JSON.stringify(json.example, null, 2); else if (json?.examples) { const first = Object.values(json.examples)[0]; if (first?.value) bodyStr = JSON.stringify(first.value, null, 2); } else if (json?.schema) bodyStr = JSON.stringify(json.schema, null, 2); responses.push({ name: `${code} response`, originalRequest: undefined, status: resp.description || 'OK', code: statusCode, header: [], body: bodyStr, }); } const item = { name: `${httpMethod} ${pmPath}`, request: { method: httpMethod, header: [], body, url: { raw: `{{baseUrl}}${pmPath}`, host: ['{{baseUrl}}'], path: urlPath, query: queryParams.length ? queryParams : undefined, }, }, response: responses.length ? responses : undefined, }; for (const tag of tagList) { ensureFolder(tag).push(item); } } } // Build top-level folder items const collectionItems = Array.from(folders.entries()).map(([tag, tagItems]) => ({ name: tag, item: tagItems, })); return { info: { name, schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' }, item: collectionItems, }; } function extractFormDataFromSchema(schema) { // Basic schema -> Postman formdata mapping if (!schema || schema.type !== 'object' || !schema.properties) return []; const required = schema.required || []; return Object.entries(schema.properties).map(([key, prop]) => { const isFile = prop.format === 'binary' || prop.type === 'string' && prop.format === 'byte'; return { key, type: isFile ? 'file' : 'text', value: isFile ? '' : '', description: prop.description, disabled: required.includes(key) ? false : false, }; }); } function extractUrlEncodedFromSchema(schema) { if (!schema || schema.type !== 'object' || !schema.properties) return []; return Object.entries(schema.properties).map(([key, prop]) => ({ key, value: '', description: prop.description, type: 'text', })); } export function saveCollectionToFile(collection, outputPath) { fs.writeFileSync(outputPath, JSON.stringify(collection, null, 2)); console.log(`✅ Postman collection saved to ${outputPath}`); }