571 lines
20 KiB
JavaScript
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
|
|
};
|