corrad-af-2024/server/api/openapi/generate-from-collections.post.js

381 lines
9.3 KiB
JavaScript

import fs from 'fs'
import path from 'path'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { collections, config } = body
if (!collections || !Array.isArray(collections)) {
throw createError({
statusCode: 400,
statusMessage: 'Collections array is required'
})
}
// Set appropriate headers
setHeader(event, 'Content-Type', 'application/json')
// Generate OpenAPI specification from collections
const openApiSpec = generateOpenApiFromCollections(collections, config)
// Save the generated spec to file
await saveOpenApiToFile(openApiSpec, 'openapi-collection.json')
return {
statusCode: 200,
message: 'OpenAPI specification generated successfully',
data: {
filename: 'openapi-collection.json',
url: '/openapi-coll.json',
spec: openApiSpec
}
}
} catch (error) {
console.error('Error generating OpenAPI from collections:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to generate OpenAPI specification',
data: {
error: error.message
}
})
}
})
async function saveOpenApiToFile(spec, filename) {
const docsDir = path.join(process.cwd(), 'docs')
// Ensure docs directory exists
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true })
}
const filePath = path.join(docsDir, filename)
fs.writeFileSync(filePath, JSON.stringify(spec, null, 2), 'utf-8')
}
function generateOpenApiFromCollections(collections, config = {}) {
const spec = {
openapi: '3.0.3',
info: {
title: config.title || 'Generated API Documentation from Collections',
description: config.description || 'API documentation generated from saved collections',
version: config.version || '1.0.0',
contact: config.contact || {
name: 'API Support',
email: 'support@example.com'
}
},
servers: [
{
url: '{protocol}://{host}:{port}/api',
description: 'API Server',
variables: {
protocol: {
enum: ['http', 'https'],
default: 'http'
},
host: {
default: 'localhost'
},
port: {
default: '3000'
}
}
}
],
tags: [],
paths: {},
components: {
schemas: {
SuccessResponse: {
type: 'object',
properties: {
statusCode: {
type: 'integer',
example: 200
},
message: {
type: 'string',
example: 'Operation successful'
},
data: {
type: 'object',
additionalProperties: true
}
}
},
ErrorResponse: {
type: 'object',
properties: {
statusCode: {
type: 'integer',
example: 400
},
message: {
type: 'string',
example: 'Error message'
},
errors: {
type: 'object',
additionalProperties: {
type: 'array',
items: {
type: 'string'
}
}
}
}
}
},
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT Bearer token authentication'
}
}
},
security: [
{
bearerAuth: []
}
]
}
// Create tags from collections
spec.tags = collections.map(collection => ({
name: slugify(collection.name),
description: collection.description || `Endpoints from ${collection.name} collection`
}))
// Process each collection and its requests
collections.forEach(collection => {
const tagName = slugify(collection.name)
collection.requests.forEach(request => {
const path = extractPathFromUrl(request.url)
const method = request.method.toLowerCase()
if (!spec.paths[path]) {
spec.paths[path] = {}
}
// Generate operation from request
const operation = generateOperationFromRequest(request, tagName)
spec.paths[path][method] = operation
})
})
return spec
}
function generateOperationFromRequest(request, tagName) {
const operation = {
tags: [tagName],
summary: request.name || `${request.method} request`,
description: request.description || `${request.method} request to ${request.url}`,
operationId: generateOperationId(request.method, request.name),
parameters: [],
responses: {
200: {
description: 'Successful response',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/SuccessResponse'
}
}
}
},
400: {
description: 'Bad request',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
}
}
}
},
401: {
description: 'Unauthorized',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
}
}
}
},
500: {
description: 'Internal server error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
}
}
}
}
}
}
// Add query parameters
if (request.params && Array.isArray(request.params)) {
request.params.forEach(param => {
if (param.active && param.key) {
operation.parameters.push({
name: param.key,
in: 'query',
description: param.description || `${param.key} parameter`,
required: false,
schema: {
type: 'string',
example: param.value || ''
}
})
}
})
}
// Add headers as parameters
if (request.headers && Array.isArray(request.headers)) {
request.headers.forEach(header => {
if (header.active && header.key && !isStandardHeader(header.key)) {
operation.parameters.push({
name: header.key,
in: 'header',
description: header.description || `${header.key} header`,
required: false,
schema: {
type: 'string',
example: header.value || ''
}
})
}
})
}
// Add request body for POST, PUT, PATCH methods
if (['post', 'put', 'patch'].includes(request.method.toLowerCase())) {
if (request.body && request.body.raw) {
let bodySchema
try {
// Try to parse JSON and infer schema
const parsedBody = JSON.parse(request.body.raw)
bodySchema = inferSchemaFromObject(parsedBody)
} catch {
// Fallback to string if not valid JSON
bodySchema = {
type: 'string',
example: request.body.raw
}
}
operation.requestBody = {
required: true,
content: {
'application/json': {
schema: bodySchema
}
}
}
}
}
// Add security if auth is configured
if (request.auth && request.auth.type !== 'none') {
operation.security = [{ bearerAuth: [] }]
}
return operation
}
function extractPathFromUrl(url) {
try {
const urlObj = new URL(url)
return urlObj.pathname
} catch {
// If URL is invalid, try to extract path-like string
const pathMatch = url.match(/(?:https?:\/\/[^\/]+)?(.*)/)
return pathMatch ? pathMatch[1] : url
}
}
function slugify(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
function generateOperationId(method, name) {
const cleanName = name.replace(/[^a-zA-Z0-9]/g, '')
return `${method.toLowerCase()}${cleanName}`
}
function isStandardHeader(headerName) {
const standardHeaders = [
'accept', 'accept-encoding', 'accept-language', 'authorization',
'cache-control', 'connection', 'content-length', 'content-type',
'cookie', 'host', 'user-agent', 'referer', 'origin'
]
return standardHeaders.includes(headerName.toLowerCase())
}
function inferSchemaFromObject(obj) {
if (obj === null) {
return { type: 'null' }
}
if (Array.isArray(obj)) {
return {
type: 'array',
items: obj.length > 0 ? inferSchemaFromObject(obj[0]) : { type: 'string' }
}
}
if (typeof obj === 'object') {
const properties = {}
const required = []
Object.keys(obj).forEach(key => {
properties[key] = inferSchemaFromObject(obj[key])
if (obj[key] !== null && obj[key] !== undefined) {
required.push(key)
}
})
const schema = {
type: 'object',
properties
}
if (required.length > 0) {
schema.required = required
}
return schema
}
if (typeof obj === 'string') {
return { type: 'string', example: obj }
}
if (typeof obj === 'number') {
return Number.isInteger(obj)
? { type: 'integer', example: obj }
: { type: 'number', example: obj }
}
if (typeof obj === 'boolean') {
return { type: 'boolean', example: obj }
}
return { type: 'string' }
}