381 lines
9.3 KiB
JavaScript
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' }
|
|
}
|