apx-toolkit
Version:
Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.
512 lines • 19.2 kB
JavaScript
import { generateJSONSchema, createSchemaRef } from './json-schema-generator.js';
/**
* Generates OpenAPI 3.1 specification from discovered APIs
* Enhanced with JSON Schema validation, security schemes, and best practices
*/
export function generateOpenAPISpec(apis, baseUrl, responseExamples) {
const spec = {
openapi: '3.1.0', // Upgraded to 3.1.0 for better JSON Schema support
info: {
title: 'Discovered API',
description: 'Auto-generated API documentation from discovered endpoints. Generated by APX Toolkit.',
version: '1.0.0',
contact: {
name: 'APX Toolkit',
url: 'https://github.com/irun2themoney/apx-toolkit',
},
license: {
name: 'ISC',
},
},
servers: baseUrl
? [{ url: baseUrl, description: 'API Server' }]
: apis.length > 0
? [{ url: new URL(apis[0].baseUrl).origin, description: 'API Server' }]
: [],
paths: {},
components: {
schemas: {},
securitySchemes: {},
},
tags: [],
};
// Track schemas for reuse (JSON Schema best practice: use $ref)
const schemaMap = new Map();
for (const api of apis) {
const url = new URL(api.baseUrl);
const path = url.pathname;
const method = api.method.toLowerCase();
if (!spec.paths[path]) {
spec.paths[path] = {};
}
const operation = {
summary: `Discovered ${api.method} endpoint`,
description: `Auto-discovered API endpoint from ${api.url}`,
operationId: `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`,
tags: ['Discovered APIs'],
};
// Add query parameters
if (api.queryParams && Object.keys(api.queryParams).length > 0) {
operation.parameters = Object.entries(api.queryParams).map(([key, value]) => ({
name: key,
in: 'query',
required: false,
schema: {
type: typeof value === 'number' ? 'number' : 'string',
example: value,
},
description: inferFieldDescription(key, value),
}));
}
// Add pagination parameters
if (api.paginationInfo) {
const paramName = api.paginationInfo.paramName || 'page';
if (!operation.parameters) {
operation.parameters = [];
}
operation.parameters.push({
name: paramName,
in: 'query',
required: false,
schema: {
type: api.paginationInfo.type === 'cursor' ? 'string' : 'integer',
example: api.paginationInfo.currentPage || api.paginationInfo.currentOffset || 1,
},
description: `Pagination parameter (${api.paginationInfo.type}). Used to navigate through paginated results.`,
});
}
// Add request body for POST with JSON Schema
if (api.method === 'POST' && api.body) {
const requestSchemaName = `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}_Request`;
const requestSchema = generateJSONSchema(api.body, requestSchemaName);
// Store in components for reuse
const schemaKey = requestSchemaName.replace(/[^a-zA-Z0-9]/g, '_');
if (!schemaMap.has(schemaKey)) {
schemaMap.set(schemaKey, requestSchema);
spec.components.schemas[schemaKey] = requestSchema;
}
operation.requestBody = {
required: true,
description: 'Request body',
content: {
'application/json': {
schema: createSchemaRef(schemaKey),
example: api.body,
},
},
};
}
// Add headers
if (api.headers && Object.keys(api.headers).length > 0) {
if (!operation.parameters) {
operation.parameters = [];
}
for (const [key, value] of Object.entries(api.headers)) {
if (key.toLowerCase() !== 'content-type') {
operation.parameters.push({
name: key,
in: 'header',
required: false,
schema: {
type: 'string',
example: value,
},
description: `Header: ${key}`,
});
}
}
}
// Add response schema with JSON Schema validation
const responseExample = responseExamples?.get(api.url);
let responseSchema = {
type: 'object',
description: 'API response (structure discovered from actual responses)',
};
// Generate proper JSON Schema from response example if available
if (responseExample) {
const schemaName = `${method}_${path.replace(/[^a-zA-Z0-9]/g, '_')}_Response`;
const jsonSchema = generateJSONSchema(responseExample, schemaName);
// Store schema in components for reuse ($ref best practice)
const schemaKey = schemaName.replace(/[^a-zA-Z0-9]/g, '_');
if (!schemaMap.has(schemaKey)) {
schemaMap.set(schemaKey, jsonSchema);
spec.components.schemas[schemaKey] = jsonSchema;
}
// Use $ref for reusability (JSON Schema best practice)
responseSchema = createSchemaRef(schemaKey);
}
operation.responses = {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: responseSchema,
example: responseExample || undefined,
},
},
},
'400': {
description: 'Bad Request',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' },
},
},
},
},
},
'401': {
description: 'Unauthorized',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: { type: 'string', example: 'Unauthorized' },
},
},
},
},
},
'500': {
description: 'Internal Server Error',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' },
},
},
},
},
},
};
// Add security schemes if authentication detected
const hasAuth = api.headers && (api.headers['authorization'] ||
api.headers['Authorization'] ||
api.headers['x-api-key'] ||
api.headers['X-API-Key']);
if (hasAuth) {
// Detect authentication type
if (api.headers['authorization'] || api.headers['Authorization']) {
if (!spec.components.securitySchemes['bearerAuth']) {
spec.components.securitySchemes['bearerAuth'] = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Bearer token authentication',
};
}
operation.security = [{ bearerAuth: [] }];
}
else if (api.headers['x-api-key'] || api.headers['X-API-Key']) {
if (!spec.components.securitySchemes['apiKeyAuth']) {
spec.components.securitySchemes['apiKeyAuth'] = {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
description: 'API key authentication',
};
}
operation.security = [{ apiKeyAuth: [] }];
}
}
spec.paths[path][method] = operation;
}
// Add tags for better organization
if (spec.tags.length === 0) {
spec.tags.push({
name: 'Discovered APIs',
description: 'Auto-discovered API endpoints',
});
}
// Clean up empty components
if (Object.keys(spec.components.schemas).length === 0) {
delete spec.components.schemas;
}
if (Object.keys(spec.components.securitySchemes).length === 0) {
delete spec.components.securitySchemes;
}
if (Object.keys(spec.components).length === 0) {
delete spec.components;
}
return JSON.stringify(spec, null, 2);
}
/**
* Infers human-readable descriptions for API fields based on naming patterns
*/
function inferFieldDescription(fieldName, exampleValue) {
const name = fieldName.toLowerCase();
// Common field patterns
const patterns = [
[/^id$|_id$|Id$/, 'A unique identifier'],
[/^name$|_name$/, 'The name of the item'],
[/^email$|_email$/, 'An email address'],
[/^url$|_url$|Url$/, 'A URL or web address'],
[/^date$|_date$|Date$/, 'A date value'],
[/^time$|_time$|Time$|timestamp$/, 'A timestamp or time value'],
[/^page$|_page$/, 'Page number for pagination'],
[/^limit$|_limit$/, 'Maximum number of items to return'],
[/^offset$|_offset$/, 'Number of items to skip'],
[/^total$|_total$/, 'Total number of items'],
[/^count$|_count$/, 'Count of items'],
[/^status$|_status$/, 'Status of the item'],
[/^type$|_type$/, 'Type or category of the item'],
[/^title$|_title$/, 'Title of the item'],
[/^description$|_description$/, 'Description of the item'],
[/^created$|_created$|created_at$/, 'Creation timestamp'],
[/^updated$|_updated$|updated_at$/, 'Last update timestamp'],
[/^user$|_user$/, 'User information'],
[/^token$|_token$/, 'Authentication or access token'],
[/^key$|_key$/, 'API key or identifier'],
];
for (const [pattern, description] of patterns) {
if (pattern.test(name)) {
return description;
}
}
// Default description based on type
if (typeof exampleValue === 'number') {
return `Numeric value: ${fieldName}`;
}
else if (typeof exampleValue === 'string') {
return `String value: ${fieldName}`;
}
else if (typeof exampleValue === 'boolean') {
return `Boolean flag: ${fieldName}`;
}
return `Query parameter: ${fieldName}`;
}
/**
* Generates Postman collection from discovered APIs
*/
export function generatePostmanCollection(apis, collectionName = 'Discovered APIs') {
const collection = {
info: {
name: collectionName,
description: 'Auto-generated Postman collection from discovered API endpoints',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
},
item: [],
};
for (const api of apis) {
const url = new URL(api.baseUrl);
const queryParams = [];
// Add query parameters
if (api.queryParams) {
for (const [key, value] of Object.entries(api.queryParams)) {
queryParams.push({
key,
value: String(value),
});
}
}
// Add pagination parameters
if (api.paginationInfo) {
const paramName = api.paginationInfo.paramName || 'page';
queryParams.push({
key: paramName,
value: String(api.paginationInfo.currentPage ||
api.paginationInfo.currentOffset ||
1),
description: `Pagination: ${api.paginationInfo.type}`,
});
}
const request = {
name: `${api.method} ${url.pathname}`,
request: {
method: api.method,
header: [],
url: {
raw: api.baseUrl + (queryParams.length > 0 ? '?' : ''),
protocol: url.protocol.slice(0, -1),
host: url.hostname.split('.'),
path: url.pathname.split('/').filter(Boolean),
query: queryParams,
},
},
response: [],
};
// Add headers
if (api.headers) {
for (const [key, value] of Object.entries(api.headers)) {
request.request.header.push({
key,
value,
});
}
}
// Add body for POST
if (api.method === 'POST' && api.body) {
request.request.body = {
mode: 'raw',
raw: JSON.stringify(api.body, null, 2),
options: {
raw: {
language: 'json',
},
},
};
}
collection.item.push(request);
}
return JSON.stringify(collection, null, 2);
}
/**
* Generates cURL commands from discovered APIs
*/
export function generateCurlCommands(apis) {
const commands = [];
for (const api of apis) {
const url = new URL(api.baseUrl);
let curl = `curl -X ${api.method}`;
// Add headers
if (api.headers) {
for (const [key, value] of Object.entries(api.headers)) {
curl += ` \\\n -H "${key}: ${value}"`;
}
}
// Add query parameters
const queryParams = [];
if (api.queryParams) {
for (const [key, value] of Object.entries(api.queryParams)) {
queryParams.push(`${key}=${encodeURIComponent(String(value))}`);
}
}
// Add pagination parameters
if (api.paginationInfo) {
const paramName = api.paginationInfo.paramName || 'page';
const paramValue = api.paginationInfo.currentPage ||
api.paginationInfo.currentOffset ||
1;
queryParams.push(`${paramName}=${paramValue}`);
}
const fullUrl = api.baseUrl + (queryParams.length > 0 ? '?' + queryParams.join('&') : '');
curl += ` \\\n "${fullUrl}"`;
// Add body for POST
if (api.method === 'POST' && api.body) {
curl += ` \\\n -d '${JSON.stringify(api.body)}'`;
curl += ` \\\n -H "Content-Type: application/json"`;
}
commands.push(curl);
commands.push(''); // Empty line between commands
}
return commands.join('\n');
}
/**
* Generates Insomnia workspace from discovered APIs
*/
export function generateInsomniaWorkspace(apis, workspaceName = 'Discovered APIs') {
const workspace = {
_type: 'export',
__export_format: 4,
__export_date: new Date().toISOString(),
__export_source: 'smart-api-finder-documenter',
resources: [
{
_id: 'wrk_discovered',
_type: 'workspace',
name: workspaceName,
description: 'Auto-generated Insomnia workspace from discovered APIs',
},
],
};
for (let i = 0; i < apis.length; i++) {
const api = apis[i];
const url = new URL(api.baseUrl);
const queryParams = [];
// Add query parameters
if (api.queryParams) {
for (const [key, value] of Object.entries(api.queryParams)) {
queryParams.push({
name: key,
value: String(value),
});
}
}
// Add pagination parameters
if (api.paginationInfo) {
const paramName = api.paginationInfo.paramName || 'page';
queryParams.push({
name: paramName,
value: String(api.paginationInfo.currentPage ||
api.paginationInfo.currentOffset ||
1),
});
}
const request = {
_id: `req_${i}`,
_type: 'request',
parentId: 'wrk_discovered',
name: `${api.method} ${url.pathname}`,
url: api.baseUrl,
method: api.method,
headers: [],
parameters: queryParams,
};
// Add headers
if (api.headers) {
for (const [key, value] of Object.entries(api.headers)) {
request.headers.push({
name: key,
value,
});
}
}
// Add body for POST
if (api.method === 'POST' && api.body) {
request.body = {
mimeType: 'application/json',
text: JSON.stringify(api.body, null, 2),
};
}
workspace.resources.push(request);
}
return JSON.stringify(workspace, null, 2);
}
/**
* Generates all export formats for discovered APIs
*/
export function generateExports(apis, formats = ['openapi', 'postman', 'curl'], baseUrl, responseExamples) {
const exports = [];
if (formats.includes('openapi')) {
exports.push({
format: 'openapi',
content: generateOpenAPISpec(apis, baseUrl, responseExamples),
filename: 'api-spec.json',
mimeType: 'application/json',
});
}
if (formats.includes('postman')) {
exports.push({
format: 'postman',
content: generatePostmanCollection(apis),
filename: 'postman-collection.json',
mimeType: 'application/json',
});
}
if (formats.includes('curl')) {
exports.push({
format: 'curl',
content: generateCurlCommands(apis),
filename: 'curl-commands.sh',
mimeType: 'text/plain',
});
}
if (formats.includes('insomnia')) {
exports.push({
format: 'insomnia',
content: generateInsomniaWorkspace(apis),
filename: 'insomnia-workspace.json',
mimeType: 'application/json',
});
}
return exports;
}
//# sourceMappingURL=api-exporter.js.map