weaver-frontend-cli
Version:
🕷️ Weaver CLI - Generador completo de arquitectura Clean Architecture con parser OpenAPI avanzado para entidades CRUD y flujos de negocio complejos
920 lines • 48.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SwaggerAnalyzer = void 0;
const axios_1 = __importDefault(require("axios"));
const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
class SwaggerAnalyzer {
constructor() {
this.openApiDoc = null;
this.detectedApiName = null;
}
async loadFromUrl(url) {
try {
console.log(`🔍 Descargando OpenAPI desde: ${url}`);
const response = await axios_1.default.get(url);
this.openApiDoc = await swagger_parser_1.default.validate(response.data);
this.detectedApiName = this.detectApiName();
console.log(`✅ OpenAPI cargado exitosamente`);
if (this.detectedApiName) {
console.log(`🔗 API detectada: ${this.detectedApiName}`);
}
}
catch (error) {
throw new Error(`Error cargando OpenAPI: ${error}`);
}
}
getAvailableEntities() {
if (!this.openApiDoc?.paths || !this.openApiDoc?.components?.schemas) {
return [];
}
const allTags = new Set();
const tagOperations = new Map();
// Recopilar todos los tags y sus operaciones
for (const path in this.openApiDoc.paths) {
const pathItem = this.openApiDoc.paths[path];
if (!pathItem || typeof pathItem !== 'object')
continue;
const methods = ['get', 'post', 'put', 'delete', 'patch'];
for (const method of methods) {
const operation = pathItem[method];
if (operation && operation.tags) {
operation.tags.forEach(tag => {
allTags.add(tag);
if (!tagOperations.has(tag)) {
tagOperations.set(tag, new Set());
}
// Mapear método HTTP a operación CRUD
const crudOperation = this.mapHttpMethodToCrud(method, path);
if (crudOperation) {
tagOperations.get(tag).add(crudOperation);
}
});
}
}
}
// Filtrar tags que SÍ tienen CRUD completo (son entidades)
const entities = [];
for (const tag of allTags) {
const operations = tagOperations.get(tag) || new Set();
const hasCompleteCrud = this.hasCompleteCrudOperations(operations);
if (hasCompleteCrud) {
entities.push(tag);
}
}
return entities.sort();
}
/**
* Obtiene los servicios de negocio disponibles basándose en tags que NO tienen CRUD completo
* o que tienen diferente a 5 métodos HTTP
*/
getAvailableBusinessServices() {
if (!this.openApiDoc?.paths) {
return [];
}
const allTags = new Set();
const tagOperations = new Map();
const tagHttpMethods = new Map(); // Contar métodos HTTP únicos
// Recopilar todos los tags y sus operaciones
for (const path in this.openApiDoc.paths) {
const pathItem = this.openApiDoc.paths[path];
if (!pathItem || typeof pathItem !== 'object')
continue;
const methods = ['get', 'post', 'put', 'delete', 'patch'];
for (const method of methods) {
const operation = pathItem[method];
if (operation && operation.tags) {
operation.tags.forEach(tag => {
allTags.add(tag);
if (!tagOperations.has(tag)) {
tagOperations.set(tag, new Set());
}
if (!tagHttpMethods.has(tag)) {
tagHttpMethods.set(tag, new Set());
}
// Contar método HTTP único
tagHttpMethods.get(tag).add(method.toUpperCase());
// Mapear método HTTP a operación CRUD
const crudOperation = this.mapHttpMethodToCrud(method, path);
if (crudOperation) {
tagOperations.get(tag).add(crudOperation);
}
});
}
}
}
// Filtrar tags que NO tienen CRUD completo O tienen diferente a 5 métodos HTTP (son servicios de negocio)
const businessServices = [];
for (const tag of allTags) {
const operations = tagOperations.get(tag) || new Set();
const hasCompleteCrud = this.hasCompleteCrudOperations(operations);
// Contar métodos HTTP únicos
const httpMethodsCount = tagHttpMethods.get(tag)?.size || 0;
const hasDifferentHttpMethodsCount = httpMethodsCount !== 5; // Diferente de 5
// Es flujo de negocio si: NO tiene CRUD completo O tiene diferente a 5 métodos HTTP
if (!hasCompleteCrud || hasDifferentHttpMethodsCount) {
businessServices.push(tag);
}
}
return businessServices.sort();
}
/**
* Mapea un método HTTP y path a una operación CRUD
*/
mapHttpMethodToCrud(method, path) {
switch (method.toLowerCase()) {
case 'post':
// POST /entity/list es list, no save
if (path.includes('/list')) {
return 'list';
}
return 'save';
case 'get':
// Si el path tiene parámetros, es read; si no, es list
return (path.includes('{') || path.includes('/:')) ? 'read' : 'list';
case 'put':
case 'patch':
return 'update';
case 'delete':
return 'delete';
default:
return null;
}
}
/**
* Verifica si un conjunto de operaciones incluye CRUD completo
*/
hasCompleteCrudOperations(operations) {
const requiredOperations = ['save', 'update', 'list', 'delete', 'read'];
return requiredOperations.every(op => operations.has(op));
}
/**
* Obtiene el schema de un servicio de negocio basándose en sus operaciones Request/Response
*/
getBusinessServiceSchema(serviceName) {
if (!this.openApiDoc?.paths) {
return null;
}
const operations = {
create: false,
read: false,
update: false,
delete: false,
list: false
};
const businessOperations = [];
let description = '';
// Buscar operaciones relacionadas con este servicio
for (const path in this.openApiDoc.paths) {
const pathItem = this.openApiDoc.paths[path];
if (!pathItem || typeof pathItem !== 'object')
continue;
const methods = ['get', 'post', 'put', 'delete', 'patch'];
for (const method of methods) {
const operation = pathItem[method];
if (operation && operation.tags && operation.tags.includes(serviceName)) {
// Determinar tipo de operación
if (method === 'post')
operations.create = true;
if (method === 'get') {
if (path.includes('{') || path.includes('/:')) {
operations.read = true;
}
else {
operations.list = true;
}
}
if (method === 'put' || method === 'patch')
operations.update = true;
if (method === 'delete')
operations.delete = true;
// Obtener descripción
if (operation.description && !description) {
description = operation.description;
}
// Extraer información de la operación de negocio
const businessOp = {
operationId: operation.operationId,
method: method.toUpperCase(),
path: path,
summary: operation.summary,
requestSchema: null,
responseSchema: null,
fields: [],
pathParameters: [] // Nuevo: almacenar parámetros de path por separado
};
// 🆕 Extraer parámetros de PATH (como {user_id})
if (operation.parameters && Array.isArray(operation.parameters)) {
for (const param of operation.parameters) {
const paramObj = param;
// Solo procesar parámetros de path (no query ni header)
if (paramObj.in === 'path') {
const paramField = {
name: paramObj.name,
type: this.getTypeFromSchema(paramObj.schema || { type: 'string' }),
required: paramObj.required !== false, // Path params son siempre requeridos
format: paramObj.schema?.format,
description: paramObj.description,
isPathParam: true // Marcador para identificar que es un path param
};
businessOp.fields.push(paramField);
businessOp.pathParameters.push(paramField);
}
}
}
// Extraer schema del requestBody
if (operation.requestBody && 'content' in operation.requestBody) {
const content = operation.requestBody.content;
if (content['application/json'] && content['application/json'].schema) {
const schema = content['application/json'].schema;
if (schema.$ref) {
businessOp.requestSchema = schema.$ref.split('/').pop();
}
// Extraer campos del request con fallback a $ref
let requestSchema = schema;
// Si NO tiene properties directas pero SÍ tiene $ref, resolver la referencia
if (!schema.properties && schema.$ref) {
const refName = schema.$ref.split('/').pop();
if (this.openApiDoc?.components?.schemas && refName) {
const resolvedSchema = this.openApiDoc.components.schemas[refName];
if (resolvedSchema && resolvedSchema.properties) {
requestSchema = resolvedSchema;
console.log(` 📦 Resolviendo $ref request: ${refName} (${Object.keys(resolvedSchema.properties).length} campos)`);
}
}
}
// Ahora extraer las properties (ya sea del schema original o del resuelto)
if (requestSchema && requestSchema.properties) {
for (const [fieldName, fieldSchema] of Object.entries(requestSchema.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
// Usar parseFieldSchemaWithRefs para manejar $ref anidados
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, requestSchema.required || []);
businessOp.fields.push(field);
}
}
}
}
}
// Extraer schema de la response (buscar 200, 201, o el primer código de éxito 2xx)
const successResponse = operation.responses?.['200'] || operation.responses?.['201'] ||
Object.entries(operation.responses || {}).find(([code]) => code.startsWith('2'))?.[1];
if (successResponse) {
const responseObj = successResponse;
if ('content' in responseObj && responseObj.content && responseObj.content['application/json'] && responseObj.content['application/json'].schema) {
const schema = responseObj.content['application/json'].schema;
// Manejar tanto referencias ($ref) como schemas expandidos
let responseSchemaObj = null;
let responseSchemaName = null;
if (schema.$ref) {
// Schema por referencia
responseSchemaName = schema.$ref.split('/').pop();
businessOp.responseSchema = responseSchemaName;
if (this.openApiDoc?.components?.schemas && responseSchemaName) {
responseSchemaObj = this.openApiDoc.components.schemas[responseSchemaName];
}
}
else if (schema.anyOf) {
// Manejar anyOf directamente en el schema principal
console.log(' Found anyOf in main schema');
for (const subSchema of schema.anyOf) {
if (subSchema.$ref) {
responseSchemaName = subSchema.$ref.split('/').pop();
if (this.openApiDoc?.components?.schemas && responseSchemaName) {
responseSchemaObj = this.openApiDoc.components.schemas[responseSchemaName];
console.log(' Using first $ref from anyOf:', responseSchemaName);
break;
}
}
else if (subSchema.properties && Object.keys(subSchema.properties).length > 0) {
// Usar el primer schema con propiedades válidas
responseSchemaObj = subSchema;
responseSchemaName = subSchema.title || 'InlineResponse';
console.log(' Using first properties schema from anyOf:', responseSchemaName);
break;
}
}
businessOp.responseSchema = responseSchemaName || 'InlineResponse';
}
else if (schema.properties) {
// Schema expandido directamente
responseSchemaObj = schema;
businessOp.responseSchema = 'InlineResponse';
}
if (responseSchemaObj && responseSchemaObj.properties) {
businessOp.responseFields = [];
// Buscar específicamente el campo "response" y extraer su contenido
console.log(' Response schema properties:', Object.keys(responseSchemaObj.properties));
if (responseSchemaObj.properties.response) {
const responseField = responseSchemaObj.properties.response;
console.log(' Found response field, analyzing structure...');
console.log(' Response field type:', responseField.type);
console.log(' Response field has anyOf:', !!responseField.anyOf);
// Si el campo response tiene propiedades (es un objeto)
if (responseField.properties) {
for (const [fieldName, fieldSchema] of Object.entries(responseField.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, responseField.required || []);
businessOp.responseFields.push(field);
}
}
}
else if (responseField.anyOf || responseField.oneOf) {
// Manejar casos donde response tiene anyOf/oneOf
const schemas = responseField.anyOf || responseField.oneOf;
console.log(` Processing ${schemas.length} schemas in anyOf...`);
let foundSpecificSchema = false;
// Buscar el primer schema útil en anyOf (que no sea null)
for (const [index, subSchema] of schemas.entries()) {
console.log(` Schema ${index + 1}:`, {
type: subSchema.type,
hasRef: !!subSchema.$ref,
hasProperties: !!subSchema.properties,
hasItems: !!subSchema.items,
title: subSchema.title
});
if (subSchema.$ref) {
// Si es una referencia directa, resolver y usar sus propiedades
const refName = subSchema.$ref.split('/').pop();
if (this.openApiDoc?.components?.schemas && refName) {
const refSchemaObj = this.openApiDoc.components.schemas[refName];
if (refSchemaObj && refSchemaObj.properties) {
for (const [fieldName, fieldSchema] of Object.entries(refSchemaObj.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, refSchemaObj.required || []);
businessOp.responseFields.push(field);
}
}
foundSpecificSchema = true;
break;
}
}
}
else if (subSchema.properties && Object.keys(subSchema.properties).length > 0) {
// Encontramos un schema específico con propiedades directas
for (const [fieldName, fieldSchema] of Object.entries(subSchema.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, subSchema.required || []);
businessOp.responseFields.push(field);
}
}
foundSpecificSchema = true;
break;
}
else if (subSchema.type === 'array' && subSchema.items) {
// ✅ MANEJAR ARRAY CON ITEMS.PROPERTIES (como AppointmentTableResponse[])
const arrayItemSchema = subSchema.items;
console.log(' 🎯 Processing array with items.properties...');
if (arrayItemSchema.properties) {
// Extraer todos los campos del items.properties
for (const [fieldName, fieldSchema] of Object.entries(arrayItemSchema.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, arrayItemSchema.required || []);
businessOp.responseFields.push(field);
}
}
foundSpecificSchema = true;
// Marcar que la respuesta es un array
businessOp.isResponseArray = true;
console.log(` ✅ Extracted ${businessOp.responseFields.length} fields from array items`);
break;
}
}
}
// Si no encontramos schema específico, generar campos por contexto
if (!foundSpecificSchema) {
this.generateContextualResponseFields(operation.operationId || '', businessOp);
}
}
}
else {
// Si no hay campo response específico, usar todos los campos del schema
for (const [fieldName, fieldSchema] of Object.entries(responseSchemaObj.properties)) {
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
const field = this.parseFieldSchemaWithRefs(fieldName, fieldSchema, responseSchemaObj.required || []);
businessOp.responseFields.push(field);
}
}
}
}
}
}
businessOperations.push(businessOp);
}
}
}
return {
name: serviceName,
description: description || `Servicio de negocio ${serviceName}`,
fields: businessOperations.length > 0 ? businessOperations[0].fields : [], // Para compatibilidad
operations,
businessOperations // Nueva propiedad con operaciones de negocio
};
}
getEntitySchema(entityName) {
if (!this.openApiDoc?.components?.schemas) {
return null;
}
const schemas = this.openApiDoc.components.schemas;
// Buscar el schema base de la entidad
const saveSchemaName = `${entityName}Save`;
const updateSchemaName = `${entityName}Update`;
const baseSchemaName = entityName;
// Priorizar el schema que tenga el campo 'id' (normalmente Update o base)
let primarySchema = schemas[updateSchemaName] || schemas[baseSchemaName] || schemas[saveSchemaName];
// Si ninguno existe, usar el primero disponible
if (!primarySchema) {
const availableSchemas = Object.keys(schemas).filter(name => name.includes(entityName) && ('properties' in (schemas[name] || {})));
if (availableSchemas.length > 0) {
primarySchema = schemas[availableSchemas[0]];
}
}
if (!primarySchema || !('properties' in primarySchema)) {
return null;
}
const fields = this.extractFields(primarySchema);
const operations = this.analyzeOperations(entityName);
return {
name: entityName,
description: primarySchema.description,
fields,
operations
};
}
isEntitySchema(schemaName) {
// Excluir schemas que claramente no son entidades
const excludePatterns = [
'Response', 'Error', 'HTTPValidationError', 'ValidationError',
'Pagination', 'MESSAGE_TYPE', 'NOTIFICATION_TYPE'
];
if (excludePatterns.some(pattern => schemaName.includes(pattern))) {
return false;
}
// Incluir schemas que terminan en Save, Update o que están en operaciones CRUD
return schemaName.endsWith('Save') ||
schemaName.endsWith('Update') ||
this.hasEntityOperations(schemaName);
}
extractEntityName(schemaName) {
// Extraer nombre de la entidad removiendo sufijos
if (schemaName.endsWith('Save')) {
return schemaName.replace('Save', '');
}
if (schemaName.endsWith('Update')) {
return schemaName.replace('Update', '');
}
// Si es un schema base, verificar si tiene operaciones CRUD
if (this.hasEntityOperations(schemaName)) {
return schemaName;
}
return null;
}
hasEntityOperations(entityName) {
if (!this.openApiDoc?.paths)
return false;
const entityPath = `/${entityName.toLowerCase()}`;
const entityPaths = Object.keys(this.openApiDoc.paths).filter(path => path.includes(entityPath) || path.includes(entityName.toLowerCase()));
return entityPaths.length > 0;
}
extractFields(schema) {
const fields = [];
if (!schema.properties)
return fields;
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
if (typeof fieldSchema === 'boolean')
continue;
const field = this.parseFieldSchema(fieldName, fieldSchema, schema.required || []);
fields.push(field);
}
return fields;
}
parseFieldSchema(name, schema, requiredFields) {
let type = 'string';
let isArray = false;
let isEnum = false;
let enumValues;
// Manejar arrays
let nestedFields;
if (schema.type === 'array' && schema.items && typeof schema.items === 'object') {
isArray = true;
const itemSchema = schema.items;
// Si el item es una referencia, obtener el tipo de la referencia
if (itemSchema.$ref) {
const refName = itemSchema.$ref.split('/').pop();
type = refName || 'any';
// Extraer los campos anidados del elemento del array si es un objeto
if (this.openApiDoc?.components?.schemas && refName) {
const refSchema = this.openApiDoc.components.schemas[refName];
if (refSchema && refSchema.type === 'object' && refSchema.properties) {
nestedFields = [];
for (const [propName, propSchema] of Object.entries(refSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
const nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, refSchema.required || []);
nestedFields.push(nestedField);
}
}
}
}
}
else if (itemSchema.title && itemSchema.properties) {
// Si el item tiene title y properties (como FilterManager), usar el title como tipo
type = itemSchema.title;
// Extraer los campos anidados del inline schema
nestedFields = [];
for (const [propName, propSchema] of Object.entries(itemSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
const nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, itemSchema.required || []);
nestedFields.push(nestedField);
}
}
}
else {
type = this.getTypeFromSchema(itemSchema);
}
}
else {
type = this.getTypeFromSchema(schema);
}
// Manejar enums
if (schema.enum) {
isEnum = true;
enumValues = schema.enum.map(val => String(val));
type = 'enum';
}
// Manejar anyOf (generalmente para campos opcionales)
if (schema.anyOf) {
// Buscar el primer schema útil (que no sea null)
for (const subSchema of schema.anyOf) {
if (typeof subSchema === 'object' && subSchema.type !== 'null') {
if (subSchema.type === 'array' && subSchema.items) {
// Array en anyOf - procesar como array
isArray = true;
const itemSchema = subSchema.items;
if (itemSchema.$ref) {
const refName = itemSchema.$ref.split('/').pop();
type = refName || 'any';
}
else if (itemSchema.title && itemSchema.properties) {
// Inline schema con title (como FilterManager)
type = itemSchema.title;
// Extraer campos anidados
nestedFields = [];
for (const [propName, propSchema] of Object.entries(itemSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
const nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, itemSchema.required || []);
nestedFields.push(nestedField);
}
}
}
else {
type = this.getTypeFromSchema(itemSchema);
}
break;
}
else {
// No es array, usar lógica normal
type = this.getTypeFromSchema(subSchema);
break;
}
}
}
}
return {
name,
type,
required: requiredFields.includes(name),
format: schema.format,
description: schema.description,
maxLength: schema.maxLength,
isArray,
isEnum,
enumValues,
nestedFields
};
}
getTypeFromSchema(schema) {
if (schema.format === 'uuid4')
return 'string';
if (schema.format === 'date-time')
return 'string';
if (schema.format === 'date')
return 'string';
switch (schema.type) {
case 'string': return 'string';
case 'integer': return 'number';
case 'number': return 'number';
case 'boolean': return 'boolean';
case 'array': return 'any[]';
case 'object': return 'object';
default: return 'any';
}
}
analyzeOperations(entityName) {
if (!this.openApiDoc?.paths) {
return { create: false, read: false, update: false, delete: false, list: false };
}
const operations = {
create: false,
read: false,
update: false,
delete: false,
list: false
};
const entityLower = entityName.toLowerCase();
const entityKebab = entityName.replace(/([A-Z])/g, '-$1').toLowerCase().substring(1);
for (const [path, pathItem] of Object.entries(this.openApiDoc.paths)) {
if (!pathItem || typeof pathItem !== 'object')
continue;
// Buscar paths relacionados con la entidad
const pathLower = path.toLowerCase();
if (!pathLower.includes(entityLower) && !pathLower.includes(entityKebab)) {
continue;
}
// Analizar operaciones HTTP
if (pathItem.post)
operations.create = true;
if (pathItem.get && path.includes('{id}'))
operations.read = true;
if (pathItem.get && path.includes('/list'))
operations.list = true;
if (pathItem.put)
operations.update = true;
if (pathItem.delete)
operations.delete = true;
}
return operations;
}
/**
* Detecta el nombre de la API basándose en el título, URL base o paths
*/
detectApiName() {
if (!this.openApiDoc)
return null;
// 1. Intentar extraer del título
if (this.openApiDoc.info?.title) {
const title = this.openApiDoc.info.title.toLowerCase();
// Buscar patrones comunes
const patterns = [
/(\w+)\s+api/i, // "Platform API" -> "platform"
/api\s+(\w+)/i, // "API Platform" -> "platform"
/(\w+)\s+backend/i, // "Platform Backend" -> "platform"
/(\w+)\s+service/i, // "Platform Service" -> "platform"
/(\w+)\s+aws/i, // "Platform AWS" -> "platform"
];
for (const pattern of patterns) {
const match = title.match(pattern);
if (match && match[1] && match[1] !== 'api') {
return match[1].toLowerCase();
}
}
// Si no hay patrón, usar la primera palabra significativa
const words = title.split(/\s+/).filter(word => word.length > 2 && !['api', 'backend', 'service', 'rest'].includes(word.toLowerCase()));
if (words.length > 0) {
return words[0].toLowerCase();
}
}
// 2. Intentar extraer de los servers/base URL
if (this.openApiDoc.servers && this.openApiDoc.servers.length > 0) {
const serverUrl = this.openApiDoc.servers[0].url;
const urlMatch = serverUrl.match(/\/api\/(\w+)/i);
if (urlMatch) {
return urlMatch[1].toLowerCase();
}
}
// 3. Intentar extraer de los paths más comunes
if (this.openApiDoc.paths) {
const paths = Object.keys(this.openApiDoc.paths);
const pathSegments = paths
.flatMap(path => path.split('/'))
.filter(segment => segment && segment !== 'api' && !segment.startsWith('{'))
.map(segment => segment.toLowerCase());
// Buscar el segmento más común que no sea una entidad
const segmentCounts = {};
pathSegments.forEach(segment => {
segmentCounts[segment] = (segmentCounts[segment] || 0) + 1;
});
const sortedSegments = Object.entries(segmentCounts)
.sort(([, a], [, b]) => b - a)
.map(([segment]) => segment);
// Filtrar nombres de entidades conocidas
const entityNames = this.getAvailableEntities().map(e => e.toLowerCase());
const nonEntitySegments = sortedSegments.filter(segment => !entityNames.includes(segment) &&
segment.length > 2 &&
!['list', 'create', 'update', 'delete', 'get', 'post', 'put'].includes(segment));
if (nonEntitySegments.length > 0) {
return nonEntitySegments[0];
}
}
return null;
}
/**
* Obtiene el nombre de la API detectado
*/
getDetectedApiName() {
return this.detectedApiName;
}
/**
* Sugiere posibles nombres de API basándose en análisis
*/
suggestApiNames() {
const suggestions = [];
if (this.detectedApiName) {
suggestions.push(this.detectedApiName);
}
// Agregar sugerencias comunes
const commonApiNames = ['platform', 'api', 'core', 'backend', 'service', 'main'];
commonApiNames.forEach(name => {
if (!suggestions.includes(name)) {
suggestions.push(name);
}
});
return suggestions;
}
printEntityInfo(entityName) {
const schema = this.getEntitySchema(entityName);
if (!schema) {
console.log(`❌ No se encontró información para la entidad: ${entityName}`);
return;
}
console.log(`\n📋 Información de la entidad: ${entityName}`);
if (schema.description) {
console.log(`📝 Descripción: ${schema.description}`);
}
console.log(`\n🔧 Operaciones disponibles:`);
console.log(` • Crear: ${schema.operations.create ? '✅' : '❌'}`);
console.log(` • Leer: ${schema.operations.read ? '✅' : '❌'}`);
console.log(` • Actualizar: ${schema.operations.update ? '✅' : '❌'}`);
console.log(` • Eliminar: ${schema.operations.delete ? '✅' : '❌'}`);
console.log(` • Listar: ${schema.operations.list ? '✅' : '❌'}`);
console.log(`\n📊 Campos (${schema.fields.length}):`);
schema.fields.forEach(field => {
const required = field.required ? '🔴' : '🔵';
const array = field.isArray ? '[]' : '';
const description = field.description ? ` - ${field.description}` : '';
console.log(` ${required} ${field.name}: ${field.type}${array}${description}`);
});
}
/**
* Genera campos de respuesta basándose en el contexto de la operación
*/
generateContextualResponseFields(operationId, businessOp) {
const opId = operationId.toLowerCase();
if (opId.includes('login')) {
// Para login, típicamente se retorna información del usuario/rol
businessOp.responseFields.push({ name: 'user_id', type: 'string', required: false }, { name: 'rol_id', type: 'string', required: false }, { name: 'rol_code', type: 'string', required: false }, { name: 'permissions', type: 'string[]', required: false, isArray: true }, { name: 'token', type: 'string', required: false }, { name: 'expires_at', type: 'string', required: false });
}
else if (opId.includes('refresh')) {
// Para refresh token
businessOp.responseFields.push({ name: 'token', type: 'string', required: false }, { name: 'expires_at', type: 'string', required: false });
}
else if (opId.includes('create') && opId.includes('token')) {
// Para crear API token
businessOp.responseFields.push({ name: 'token', type: 'string', required: false }, { name: 'token_id', type: 'string', required: false }, { name: 'expires_at', type: 'string', required: false });
}
else {
// Fallback genérico
businessOp.responseFields.push({ name: 'data', type: 'any', required: false });
}
}
/**
* Parsea un campo resolviendo referencias $ref y allOf
*/
parseFieldSchemaWithRefs(name, schema, required) {
// Si tiene allOf, extraer el primer $ref (patrón común en OpenAPI)
if (schema.allOf && Array.isArray(schema.allOf)) {
for (const subSchema of schema.allOf) {
if (subSchema.$ref) {
// Resolver el $ref dentro del allOf recursivamente
return this.parseFieldSchemaWithRefs(name, subSchema, required);
}
else if (subSchema.properties) {
// Si tiene properties directas, usarlas
return this.parseFieldSchemaWithRefs(name, subSchema, required);
}
}
}
// Si es una referencia, resolverla
if (schema.$ref) {
const refName = schema.$ref.split('/').pop();
if (this.openApiDoc?.components?.schemas && refName) {
const refSchema = this.openApiDoc.components.schemas[refName];
if (refSchema) {
// Para enums o tipos simples, usar el nombre de la referencia
if (refSchema.enum) {
return {
name,
type: refName, // Usar el nombre del enum
required: required.includes(name),
isEnum: true,
enumValues: refSchema.enum
};
}
// Para objetos complejos, usar el nombre de la referencia y extraer campos anidados
if (refSchema.type === 'object') {
let nestedFields;
if (refSchema.properties) {
nestedFields = [];
for (const [propName, propSchema] of Object.entries(refSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
let nestedField;
if (propSchema.$ref) {
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, refSchema.required || []);
}
else if (propSchema.type === 'object' && propSchema.properties) {
// Recursividad para objetos anidados inline
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, refSchema.required || []);
}
else if (propSchema.type === 'array' && propSchema.items) {
// Recursividad para arrays anidados
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, refSchema.required || []);
}
else {
nestedField = this.parseFieldSchema(propName, propSchema, refSchema.required || []);
if (propSchema.title && nestedField.type === 'object') {
nestedField.type = propSchema.title;
}
}
nestedFields.push(nestedField);
}
}
}
return {
name,
type: refName,
required: required.includes(name),
nestedFields: nestedFields
};
}
// Para arrays referenciados
if (refSchema.type === 'array' && refSchema.items) {
const itemField = this.parseFieldSchemaWithRefs(name, refSchema.items, refSchema.required || []);
return {
name,
type: itemField.type,
required: required.includes(name),
isArray: true,
nestedFields: itemField.nestedFields
};
}
// Para tipos primitivos, usar el tipo base
if (refSchema.type) {
return {
name,
type: this.getTypeFromSchema(refSchema),
required: required.includes(name)
};
}
}
}
}
// Si es un objeto con propiedades directas (ya expandido por el parser de Swagger)
if (schema.type === 'object' && schema.properties) {
const nestedFields = [];
for (const [propName, propSchema] of Object.entries(schema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
let nestedField;
if (propSchema.$ref) {
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, schema.required || []);
}
else if (propSchema.type === 'object' && propSchema.properties) {
// Recursividad para objetos anidados inline
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, schema.required || []);
}
else if (propSchema.type === 'array' && propSchema.items) {
// Recursividad para arrays anidados
nestedField = this.parseFieldSchemaWithRefs(propName, propSchema, schema.required || []);
}
else {
nestedField = this.parseFieldSchema(propName, propSchema, schema.required || []);
if (propSchema.title && nestedField.type === 'object') {
nestedField.type = propSchema.title;
}
}
nestedFields.push(nestedField);
}
}
return {
name,
type: schema.title || 'object',
required: required.includes(name),
nestedFields: nestedFields
};
}
// Si es un array, manejar el tipo del elemento y sus nestedFields
if (schema.type === 'array' && schema.items) {
const itemField = this.parseFieldSchemaWithRefs(name, schema.items, required);
return {
name,
type: itemField.type,
required: required.includes(name),
isArray: true,
nestedFields: itemField.nestedFields
};
}
// Si no es una referencia, usar el parser normal
return this.parseFieldSchema(name, schema, required);
}
}
exports.SwaggerAnalyzer = SwaggerAnalyzer;
//# sourceMappingURL=swagger-parser.js.map