testes/Modulos Angular/scripts/create-domain-v2-api-analyz...

571 lines
20 KiB
JavaScript

#!/usr/bin/env node
// 🎯 CREATE-DOMAIN V2.0 - API ANALYZER
// Análise automática de APIs para geração inteligente de interfaces TypeScript
const https = require('https');
const http = require('http');
/**
* 🚀 ANALISADOR HÍBRIDO DE API
* Implementa 4 estratégias para detectar automaticamente a estrutura dos dados:
* 1. OpenAPI/Swagger
* 2. API Response Analysis
* 3. Smart Detection
* 4. Intelligent Fallback
*/
class APIAnalyzer {
constructor(baseUrl = 'https://prafrota-be-bff-tenant-api.grupopra.tech', headers = {}) {
this.baseUrl = baseUrl;
this.timeout = 10000; // 10 segundos
this.customHeaders = headers; // Headers customizados para autenticação
}
/**
* 🎯 MÉTODO PRINCIPAL - Análise Híbrida (MODO RIGOROSO)
*/
async analyzeAPI(domainName, strictMode = true) {
console.log(`🔍 Iniciando análise híbrida para domínio: ${domainName}`);
console.log(`🔒 Modo rigoroso: ${strictMode ? 'ATIVADO' : 'DESATIVADO'}`);
const results = {
strategy: null,
interface: null,
fields: [],
metadata: {},
success: false
};
// ===== ESTRATÉGIA 1: OpenAPI/Swagger =====
console.log('📋 Tentativa 1: OpenAPI/Swagger...');
try {
const swaggerResult = await this.analyzeOpenAPI(domainName);
if (swaggerResult.success) {
console.log('✅ OpenAPI/Swagger: Sucesso!');
return { ...swaggerResult, strategy: 'openapi' };
}
} catch (error) {
console.log(`⚠️ OpenAPI/Swagger falhou: ${error.message}`);
}
// ===== ESTRATÉGIA 2: Análise de Resposta (CRÍTICA) =====
console.log('🔍 Tentativa 2: Análise de resposta da API...');
try {
const responseResult = await this.analyzeAPIResponse(domainName);
if (responseResult.success) {
console.log('✅ Análise de resposta: Sucesso!');
return { ...responseResult, strategy: 'response_analysis' };
} else if (strictMode && responseResult.endpointExists) {
// 🚨 MODO RIGOROSO: Se endpoint existe mas falhou, PARAR execução
const error = new Error(`🚨 ERRO CRÍTICO: Endpoint /${domainName}/ existe mas não conseguiu acessar dados. Verifique autenticação, CORS ou estrutura da resposta.`);
error.code = 'ENDPOINT_ACCESS_FAILED';
error.endpoint = responseResult.failedEndpoint;
throw error;
}
} catch (error) {
if (error.code === 'ENDPOINT_ACCESS_FAILED') {
throw error; // Re-throw critical errors
}
console.log(`⚠️ Análise de resposta falhou: ${error.message}`);
}
// ===== ESTRATÉGIA 3: Detecção Inteligente =====
console.log('🤖 Tentativa 3: Detecção inteligente...');
try {
const smartResult = await this.smartDetection(domainName);
if (smartResult.success) {
if (strictMode) {
console.log('⚠️ AVISO: Usando Smart Detection em modo rigoroso - dados podem não ser precisos');
}
console.log('✅ Detecção inteligente: Sucesso!');
return { ...smartResult, strategy: 'smart_detection' };
}
} catch (error) {
console.log(`⚠️ Detecção inteligente falhou: ${error.message}`);
}
// ===== ESTRATÉGIA 4: Fallback Inteligente =====
if (strictMode) {
const error = new Error(`🚨 MODO RIGOROSO: Não foi possível obter dados reais da API para o domínio '${domainName}'. Todas as estratégias falharam.`);
error.code = 'STRICT_MODE_FAILURE';
throw error;
}
console.log('🔄 Estratégia 4: Fallback inteligente...');
const fallbackResult = this.intelligentFallback(domainName);
console.log('✅ Fallback aplicado com sucesso!');
return { ...fallbackResult, strategy: 'intelligent_fallback' };
}
/**
* 📋 ESTRATÉGIA 1: OpenAPI/Swagger Analysis
*/
async analyzeOpenAPI(domainName) {
const swaggerEndpoints = [
`${this.baseUrl}/api-docs`,
`${this.baseUrl}/swagger.json`,
`${this.baseUrl}/openapi.json`,
`${this.baseUrl}/docs/json`
];
for (const endpoint of swaggerEndpoints) {
try {
console.log(`🔍 Testando endpoint: ${endpoint}`);
const swaggerDoc = await this.fetchJSON(endpoint);
if (swaggerDoc && swaggerDoc.components && swaggerDoc.components.schemas) {
// Procurar schema do domínio
const possibleSchemas = [
`${this.capitalize(domainName)}`,
`${this.capitalize(domainName)}Dto`,
`${this.capitalize(domainName)}Entity`,
`${this.capitalize(domainName)}Response`
];
for (const schemaName of possibleSchemas) {
const schema = swaggerDoc.components.schemas[schemaName];
if (schema) {
console.log(`✅ Schema encontrado: ${schemaName}`);
return {
success: true,
interface: this.generateFromOpenAPISchema(domainName, schema),
fields: this.extractFieldsFromSchema(schema),
metadata: {
source: 'openapi',
schemaName: schemaName,
endpoint: endpoint
}
};
}
}
}
} catch (error) {
console.log(`⚠️ Endpoint ${endpoint} falhou: ${error.message}`);
continue;
}
}
return { success: false };
}
/**
* 🔍 ESTRATÉGIA 2: API Response Analysis (MODO RIGOROSO)
*/
async analyzeAPIResponse(domainName) {
// Testar tanto singular quanto plural
const singularDomain = domainName.endsWith('s') ? domainName.slice(0, -1) : domainName;
const pluralDomain = domainName.endsWith('s') ? domainName : `${domainName}s`;
const apiEndpoints = [
// Testar singular primeiro (mais comum em APIs REST)
`${this.baseUrl}/${singularDomain}?page=1&limit=1`,
`${this.baseUrl}/api/${singularDomain}?page=1&limit=1`,
`${this.baseUrl}/${singularDomain}`,
`${this.baseUrl}/api/${singularDomain}`,
// Depois testar plural
`${this.baseUrl}/${pluralDomain}?page=1&limit=1`,
`${this.baseUrl}/api/${pluralDomain}?page=1&limit=1`,
`${this.baseUrl}/${pluralDomain}`,
`${this.baseUrl}/api/${pluralDomain}`
];
let endpointExists = false;
let failedEndpoint = null;
for (const endpoint of apiEndpoints) {
try {
console.log(`🔍 Analisando endpoint: ${endpoint}`);
const response = await this.fetchJSON(endpoint);
if (response) {
endpointExists = true; // Endpoint respondeu, existe
// Verificar se tem estrutura de dados válida
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
const sampleObject = response.data[0];
console.log(`✅ Exemplo encontrado com ${Object.keys(sampleObject).length} campos`);
return {
success: true,
interface: this.generateFromSample(domainName, sampleObject),
fields: this.extractFieldsFromSample(sampleObject),
metadata: {
source: 'api_response',
endpoint: endpoint,
sampleSize: response.data.length,
totalCount: response.totalCount || 'unknown'
}
};
} else {
// Endpoint existe mas não tem dados ou estrutura incorreta
console.log(`⚠️ Endpoint ${endpoint} respondeu mas sem dados válidos`);
failedEndpoint = endpoint;
console.log(`📋 Estrutura da resposta:`, Object.keys(response));
if (response.data) {
console.log(`📦 response.data tipo: ${Array.isArray(response.data) ? 'array' : typeof response.data}`);
console.log(`📈 response.data.length: ${response.data.length}`);
}
}
}
} catch (error) {
if (error.message.includes('404') || error.message.includes('Not Found')) {
console.log(`⚠️ Endpoint ${endpoint} não encontrado (404)`);
} else {
console.log(`⚠️ Endpoint ${endpoint} falhou: ${error.message}`);
// Outros erros podem indicar que endpoint existe mas há problema de acesso
if (!error.message.includes('Timeout') && !error.message.includes('ECONNREFUSED')) {
endpointExists = true;
failedEndpoint = endpoint;
}
}
continue;
}
}
return {
success: false,
endpointExists,
failedEndpoint
};
}
/**
* 🤖 ESTRATÉGIA 3: Smart Detection
*/
async smartDetection(domainName) {
// Tentar detectar padrões baseados no nome do domínio
const domainPatterns = this.getDomainPatterns(domainName);
if (domainPatterns.length > 0) {
return {
success: true,
interface: this.generateFromPatterns(domainName, domainPatterns),
fields: domainPatterns,
metadata: {
source: 'smart_detection',
patterns: domainPatterns.map(p => p.name)
}
};
}
return { success: false };
}
/**
* 🔄 ESTRATÉGIA 4: Intelligent Fallback
*/
intelligentFallback(domainName) {
// Template base inteligente baseado em padrões comuns
const baseFields = [
{ name: 'id', type: 'number', required: true, description: 'Identificador único' },
{ name: 'name', type: 'string', required: true, description: 'Nome do registro' },
{ name: 'description', type: 'string', required: false, description: 'Descrição opcional' },
{ name: 'status', type: 'string', required: false, description: 'Status do registro' },
{ name: 'created_at', type: 'string', required: false, description: 'Data de criação' },
{ name: 'updated_at', type: 'string', required: false, description: 'Data de atualização' }
];
// Adicionar campos específicos baseados no nome do domínio
const specificFields = this.getSpecificFieldsByDomain(domainName);
const allFields = [...baseFields, ...specificFields];
return {
success: true,
interface: this.generateFromFields(domainName, allFields),
fields: allFields,
metadata: {
source: 'intelligent_fallback',
baseFields: baseFields.length,
specificFields: specificFields.length
}
};
}
/**
* 🏗️ GERADORES DE INTERFACE
*/
generateFromOpenAPISchema(domainName, schema) {
const className = this.capitalize(domainName);
let interfaceCode = `/**\n * 🎯 ${className} Interface - Generated from OpenAPI\n * \n * Auto-generated from API schema\n */\nexport interface ${className} {\n`;
if (schema.properties) {
Object.entries(schema.properties).forEach(([fieldName, fieldSchema]) => {
const isRequired = schema.required && schema.required.includes(fieldName);
const optional = isRequired ? '' : '?';
const type = this.openAPITypeToTypeScript(fieldSchema);
const description = fieldSchema.description ? ` // ${fieldSchema.description}` : '';
interfaceCode += ` ${fieldName}${optional}: ${type};${description}\n`;
});
}
interfaceCode += '}';
return interfaceCode;
}
generateFromSample(domainName, sampleObject) {
const className = this.capitalize(domainName);
let interfaceCode = `/**\n * 🎯 ${className} Interface - Generated from API Response\n * \n * Auto-generated from real API data\n */\nexport interface ${className} {\n`;
Object.entries(sampleObject).forEach(([key, value]) => {
const type = this.detectTypeScript(value);
const isOptional = value === null || value === undefined;
const optional = isOptional ? '?' : '';
const description = this.inferDescription(key, type);
interfaceCode += ` ${key}${optional}: ${type}; // ${description}\n`;
});
interfaceCode += '}';
return interfaceCode;
}
generateFromPatterns(domainName, patterns) {
const className = this.capitalize(domainName);
let interfaceCode = `/**\n * 🎯 ${className} Interface - Generated from Smart Patterns\n * \n * Auto-generated using intelligent pattern detection\n */\nexport interface ${className} {\n`;
// 🔧 Sempre incluir campos base obrigatórios
const baseFields = [
{ name: 'id', type: 'number', required: true, description: 'Identificador único' },
{ name: 'name', type: 'string', required: false, description: 'Nome do registro' },
{ name: 'status', type: 'string', required: false, description: 'Status do registro' },
{ name: 'created_at', type: 'string', required: false, description: 'Data de criação' },
{ name: 'updated_at', type: 'string', required: false, description: 'Data de atualização' }
];
// Adicionar campos base primeiro
baseFields.forEach(field => {
const optional = field.required ? '' : '?';
interfaceCode += ` ${field.name}${optional}: ${field.type}; // ${field.description}\n`;
});
// Depois adicionar campos específicos do domínio
patterns.forEach(field => {
const optional = field.required ? '' : '?';
interfaceCode += ` ${field.name}${optional}: ${field.type}; // ${field.description}\n`;
});
interfaceCode += '}';
return interfaceCode;
}
generateFromFields(domainName, fields) {
const className = this.capitalize(domainName);
let interfaceCode = `/**\n * 🎯 ${className} Interface - Intelligent Fallback\n * \n * Generated using intelligent fallback with domain-specific patterns\n */\nexport interface ${className} {\n`;
fields.forEach(field => {
const optional = field.required ? '' : '?';
interfaceCode += ` ${field.name}${optional}: ${field.type}; // ${field.description}\n`;
});
interfaceCode += '}';
return interfaceCode;
}
/**
* 🔧 UTILITÁRIOS
*/
async fetchJSON(url, timeout = this.timeout) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https:') ? https : http;
const timeoutId = setTimeout(() => reject(new Error('Timeout')), timeout);
// Parse URL para adicionar headers
const urlParts = new URL(url);
const options = {
hostname: urlParts.hostname,
port: urlParts.port || (protocol === https ? 443 : 80),
path: urlParts.pathname + urlParts.search,
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'API-Analyzer-V2.0',
...this.customHeaders // Adicionar headers customizados
}
};
const req = protocol.request(options, (res) => {
clearTimeout(timeoutId);
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(new Error('Invalid JSON'));
}
});
});
req.on('error', (error) => {
clearTimeout(timeoutId);
reject(error);
});
req.end();
});
}
detectTypeScript(value) {
if (value === null || value === undefined) return 'any';
const type = typeof value;
if (type === 'string') {
// Detectar datas
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'string';
// Detectar emails
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'string';
return 'string';
}
if (type === 'number') {
return Number.isInteger(value) ? 'number' : 'number';
}
if (type === 'boolean') return 'boolean';
if (Array.isArray(value)) {
if (value.length === 0) return 'any[]';
return `${this.detectTypeScript(value[0])}[]`;
}
if (type === 'object') {
return 'any'; // Para objetos complexos
}
return 'any';
}
openAPITypeToTypeScript(schema) {
if (schema.type === 'integer' || schema.type === 'number') return 'number';
if (schema.type === 'string') return 'string';
if (schema.type === 'boolean') return 'boolean';
if (schema.type === 'array') {
const itemType = schema.items ? this.openAPITypeToTypeScript(schema.items) : 'any';
return `${itemType}[]`;
}
if (schema.type === 'object') return 'any';
return 'any';
}
inferDescription(fieldName, type) {
const descriptions = {
id: 'Identificador único',
name: 'Nome do registro',
title: 'Título',
description: 'Descrição',
status: 'Status do registro',
active: 'Se está ativo',
created_at: 'Data de criação',
updated_at: 'Data de atualização',
deleted_at: 'Data de exclusão',
email: 'Endereço de email',
phone: 'Número de telefone',
address: 'Endereço',
price: 'Preço',
amount: 'Quantidade/Valor',
quantity: 'Quantidade',
user_id: 'ID do usuário',
company_id: 'ID da empresa'
};
return descriptions[fieldName] || `Campo ${fieldName} (${type})`;
}
getDomainPatterns(domainName) {
const patterns = {
// Padrões para veículos
vehicle: [
{ name: 'brand', type: 'string', required: false, description: 'Marca do veículo' },
{ name: 'model', type: 'string', required: false, description: 'Modelo do veículo' },
{ name: 'year', type: 'number', required: false, description: 'Ano do veículo' },
{ name: 'plate', type: 'string', required: false, description: 'Placa do veículo' },
{ name: 'color', type: 'string', required: false, description: 'Cor do veículo' }
],
// Padrões para usuários
user: [
{ name: 'email', type: 'string', required: true, description: 'Email do usuário' },
{ name: 'password', type: 'string', required: false, description: 'Senha (hash)' },
{ name: 'role', type: 'string', required: false, description: 'Papel do usuário' },
{ name: 'avatar', type: 'string', required: false, description: 'URL do avatar' }
],
// Padrões para produtos
product: [
{ name: 'price', type: 'number', required: false, description: 'Preço do produto' },
{ name: 'category', type: 'string', required: false, description: 'Categoria do produto' },
{ name: 'stock', type: 'number', required: false, description: 'Estoque disponível' },
{ name: 'sku', type: 'string', required: false, description: 'Código SKU' }
],
// Padrões para empresas
company: [
{ name: 'cnpj', type: 'string', required: false, description: 'CNPJ da empresa' },
{ name: 'address', type: 'string', required: false, description: 'Endereço da empresa' },
{ name: 'phone', type: 'string', required: false, description: 'Telefone da empresa' }
]
};
// Buscar padrões exatos e similares
const lowerDomain = domainName.toLowerCase();
if (patterns[lowerDomain]) {
return patterns[lowerDomain];
}
// Buscar padrões parciais
for (const [pattern, fields] of Object.entries(patterns)) {
if (lowerDomain.includes(pattern) || pattern.includes(lowerDomain)) {
return fields;
}
}
return [];
}
getSpecificFieldsByDomain(domainName) {
// Retorna campos específicos baseados no nome do domínio
const specificFields = this.getDomainPatterns(domainName);
return specificFields;
}
extractFieldsFromSchema(schema) {
if (!schema.properties) return [];
return Object.entries(schema.properties).map(([name, fieldSchema]) => ({
name,
type: this.openAPITypeToTypeScript(fieldSchema),
required: schema.required && schema.required.includes(name),
description: fieldSchema.description || this.inferDescription(name, this.openAPITypeToTypeScript(fieldSchema))
}));
}
extractFieldsFromSample(sampleObject) {
return Object.entries(sampleObject).map(([name, value]) => ({
name,
type: this.detectTypeScript(value),
required: value !== null && value !== undefined,
description: this.inferDescription(name, this.detectTypeScript(value))
}));
}
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
// Função principal para uso no create-domain-v2.js
async function analyzeAPIForDomain(domainName, baseUrl, strictMode = true) {
const analyzer = new APIAnalyzer(baseUrl);
return await analyzer.analyzeAPI(domainName, strictMode);
}
module.exports = {
APIAnalyzer,
analyzeAPIForDomain
};