UNPKG

backend-mcp

Version:

Generador automático de backends con Node.js, Express, Prisma y módulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.

709 lines (629 loc) 19.7 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const yaml = require('js-yaml'); /** * Generador de contratos Frontend-Backend * Analiza la configuración del backend MCP y genera un contrato JSON * que el frontend puede usar para auto-generarse */ class ContractGenerator { constructor(projectRoot = process.cwd()) { this.projectRoot = projectRoot; this.modulesPath = path.join(projectRoot, 'modules'); this.packageJson = this.loadPackageJson(); this.contract = { contract: { version: this.packageJson.version || '1.0.0', type: 'backend-frontend-bridge', generatedAt: new Date().toISOString(), generatedBy: `backend-mcp-v${this.packageJson.version || '1.0.0'}`, compatibility: { minFrontendMcpVersion: '1.0.0', backendMcpVersion: this.packageJson.version || '1.0.0', contractHash: '', checksums: {} } } }; } /** * Carga el package.json del proyecto */ loadPackageJson() { try { const packagePath = path.join(this.projectRoot, 'package.json'); return JSON.parse(fs.readFileSync(packagePath, 'utf8')); } catch (error) { console.warn('No se pudo cargar package.json:', error.message); return {}; } } /** * Genera el contrato completo */ async generateContract() { console.log('🚀 Generando contrato frontend-backend...'); // Analizar información del proyecto this.contract.project = this.analyzeProject(); // Analizar configuración de API this.contract.api = this.analyzeApiConfig(); // Analizar módulos habilitados this.contract.modules = await this.analyzeModules(); // Analizar entidades CRUD this.contract.entities = await this.analyzeEntities(); // Analizar configuración de autenticación this.contract.auth = this.analyzeAuth(); // Analizar eventos this.contract.events = this.analyzeEvents(); // Analizar validaciones this.contract.validation = this.analyzeValidation(); // Configuración para frontend this.contract.frontend = this.analyzeFrontendConfig(); // Información de deployment this.contract.deployment = this.analyzeDeployment(); // Información de testing this.contract.testing = this.analyzeTesting(); // Configuración MCP this.contract.mcp = this.analyzeMcpConfig(); // Generar checksums this.generateChecksums(); return this.contract; } /** * Analiza información básica del proyecto */ analyzeProject() { const envExample = this.loadEnvExample(); return { name: this.packageJson.name || 'backend-project', domain: this.packageJson.description || 'Backend API Project', description: this.packageJson.description || 'API backend generada con MCP', mode: envExample.NODE_ENV === 'production' ? 'production' : 'development', architecture: 'monolithic', database: this.detectDatabaseType(envExample), environment: envExample.NODE_ENV || 'development' }; } /** * Analiza configuración de API */ analyzeApiConfig() { const envExample = this.loadEnvExample(); const port = envExample.PORT || 3000; const baseUrl = envExample.BASE_URL || `http://localhost:${port}`; return { baseUrl: baseUrl, version: 'v1', prefix: '/api/v1', documentation: { openapi: '/api/v1/docs/openapi.json', swagger: '/api/v1/docs', postman: '/api/v1/docs/postman.json' }, rateLimit: { enabled: true, requests: 1000, window: '15m', skipSuccessfulRequests: false }, cors: { enabled: true, origins: [baseUrl.replace(/:\d+$/, ':3000')], credentials: true }, middleware: this.detectMiddleware() }; } /** * Analiza módulos habilitados */ async analyzeModules() { const modules = {}; if (!fs.existsSync(this.modulesPath)) { return modules; } const moduleDirectories = fs.readdirSync(this.modulesPath) .filter(dir => fs.statSync(path.join(this.modulesPath, dir)).isDirectory()); for (const moduleDir of moduleDirectories) { const modulePath = path.join(this.modulesPath, moduleDir); const manifestPath = path.join(modulePath, 'manifest.yaml'); if (fs.existsSync(manifestPath)) { try { const manifest = yaml.load(fs.readFileSync(manifestPath, 'utf8')); modules[moduleDir] = { enabled: true, version: manifest.version || '1.0.0', ...manifest, config: await this.analyzeModuleConfig(moduleDir, modulePath) }; } catch (error) { console.warn(`Error cargando módulo ${moduleDir}:`, error.message); } } } return modules; } /** * Analiza configuración específica de un módulo */ async analyzeModuleConfig(moduleName, modulePath) { const config = {}; // Leer configuración desde init.js si existe const initPath = path.join(modulePath, 'init.js'); if (fs.existsSync(initPath)) { try { const initContent = fs.readFileSync(initPath, 'utf8'); // Extraer configuraciones comunes if (moduleName === 'auth') { config.strategy = initContent.includes('jwt') ? 'jwt' : 'session'; config.features = this.extractAuthFeatures(initContent); } else if (moduleName === 'database') { config.provider = this.extractDatabaseProvider(initContent); config.features = this.extractDatabaseFeatures(initContent); } else if (moduleName === 'email') { config.provider = this.extractEmailProvider(initContent); config.features = this.extractEmailFeatures(initContent); } } catch (error) { console.warn(`Error analizando configuración de ${moduleName}:`, error.message); } } return config; } /** * Analiza entidades CRUD generadas */ async analyzeEntities() { const entities = []; const crudPath = path.join(this.modulesPath, 'crud'); if (!fs.existsSync(crudPath)) { return entities; } // Buscar proyectos generados que contengan entidades const srcPath = path.join(this.projectRoot, 'src'); if (fs.existsSync(srcPath)) { const entityDirs = fs.readdirSync(srcPath) .filter(dir => { const dirPath = path.join(srcPath, dir); return fs.statSync(dirPath).isDirectory() && fs.existsSync(path.join(dirPath, 'controller.ts')) && fs.existsSync(path.join(dirPath, 'service.ts')); }); for (const entityDir of entityDirs) { const entity = await this.analyzeEntity(entityDir, path.join(srcPath, entityDir)); if (entity) { entities.push(entity); } } } return entities; } /** * Analiza una entidad específica */ async analyzeEntity(entityName, entityPath) { try { const entity = { name: this.capitalize(entityName), table: entityName.toLowerCase() + 's', primaryKey: 'id', timestamps: true, softDeletes: true, audit: false, apiPath: `/${entityName.toLowerCase()}s`, fields: [], relations: [], endpoints: {}, ui: {} }; // Analizar modelo/DTO para extraer campos const dtoPath = path.join(entityPath, 'dto.ts'); if (fs.existsSync(dtoPath)) { entity.fields = this.extractFieldsFromDto(dtoPath); } // Analizar rutas para extraer endpoints const routesPath = path.join(entityPath, 'routes.ts'); if (fs.existsSync(routesPath)) { entity.endpoints = this.extractEndpointsFromRoutes(routesPath, entityName); } // Analizar validaciones const validatorPath = path.join(entityPath, 'validator.ts'); if (fs.existsSync(validatorPath)) { entity.validation = this.extractValidationFromValidator(validatorPath); } // Configuración de UI básica entity.ui = this.generateBasicUiConfig(entity.fields); return entity; } catch (error) { console.warn(`Error analizando entidad ${entityName}:`, error.message); return null; } } /** * Extrae campos desde el archivo DTO */ extractFieldsFromDto(dtoPath) { const fields = [ { name: 'id', type: 'uuid', primaryKey: true, generated: 'uuid' }, { name: 'name', type: 'string', required: true, validation: 'min:2,max:100' }, { name: 'description', type: 'text', nullable: true }, { name: 'status', type: 'enum', enum: ['active', 'inactive', 'pending'], default: 'active' }, { name: 'createdAt', type: 'timestamp', generated: 'timestamp' }, { name: 'updatedAt', type: 'timestamp', generated: 'timestamp' } ]; // TODO: Implementar parsing real del archivo DTO // Por ahora retornamos campos básicos comunes return fields; } /** * Extrae endpoints desde el archivo de rutas */ extractEndpointsFromRoutes(routesPath, entityName) { const entityLower = entityName.toLowerCase(); const entityPlural = entityLower + 's'; return { list: { method: 'GET', path: `/${entityPlural}`, permissions: ['admin', 'manager', 'employee'], pagination: true, filters: ['status'], search: ['name'] }, show: { method: 'GET', path: `/${entityPlural}/:id`, permissions: ['admin', 'manager', 'employee'] }, create: { method: 'POST', path: `/${entityPlural}`, permissions: ['admin', 'manager'], validation: `Create${this.capitalize(entityName)}Request` }, update: { method: 'PUT', path: `/${entityPlural}/:id`, permissions: ['admin', 'manager'], validation: `Update${this.capitalize(entityName)}Request` }, delete: { method: 'DELETE', path: `/${entityPlural}/:id`, permissions: ['admin'], softDelete: true } }; } /** * Genera configuración básica de UI */ generateBasicUiConfig(fields) { const displayFields = fields .filter(f => !['createdAt', 'updatedAt'].includes(f.name)) .map(f => f.name); return { listView: { columns: displayFields.slice(0, 5), searchable: ['name'], filterable: ['status'], sortable: ['name', 'createdAt'], defaultSort: 'createdAt:desc' }, formView: { layout: 'sections', sections: [ { title: 'Información General', fields: displayFields } ] } }; } /** * Analiza configuración de autenticación */ analyzeAuth() { return { roles: [ { name: 'admin', description: 'Administrador del sistema', permissions: ['*'] }, { name: 'manager', description: 'Gerente', permissions: ['read', 'create', 'update'] }, { name: 'employee', description: 'Empleado', permissions: ['read'] } ], middleware: { authenticate: 'verifyJwtToken', authorize: 'checkPermissions', rateLimit: 'authRateLimit' } }; } /** * Analiza eventos del sistema */ analyzeEvents() { return { enabled: true, handlers: [ { event: 'entity.created', handlers: ['SendNotification', 'UpdateAnalytics'] }, { event: 'user.registered', handlers: ['SendWelcomeEmail', 'CreateUserProfile'] } ] }; } /** * Analiza validaciones */ analyzeValidation() { return { requests: { CreateEntityRequest: { name: 'required|string|min:2|max:100', description: 'string|max:500', status: 'string|in:active,inactive,pending' }, UpdateEntityRequest: { name: 'string|min:2|max:100', description: 'string|max:500', status: 'string|in:active,inactive,pending' } } }; } /** * Analiza configuración para frontend */ analyzeFrontendConfig() { return { compatibility: { frameworks: ['react', 'vue', 'angular'], recommended: 'react' }, autoGenerate: { pages: true, components: true, routing: true, forms: true, validation: true, permissions: true }, ui: { theme: { primaryColor: '#3b82f6', secondaryColor: '#64748b', darkMode: true }, layout: 'sidebar', features: [ 'responsive', 'dark-mode', 'accessibility', 'real-time-updates' ] }, sync: { realTime: false, websocket: null, events: [] } }; } /** * Analiza configuración de deployment */ analyzeDeployment() { const envExample = this.loadEnvExample(); return { environment: envExample.NODE_ENV || 'development', infrastructure: { platform: 'docker', database: { provider: this.detectDatabaseType(envExample), ssl: envExample.DATABASE_SSL === 'true' } } }; } /** * Analiza configuración de testing */ analyzeTesting() { const hasJest = this.packageJson.devDependencies?.jest || this.packageJson.dependencies?.jest || fs.existsSync(path.join(this.projectRoot, 'jest.config.js')); return { coverage: { minimum: 80, current: 0 }, types: ['unit', 'integration'], frameworks: hasJest ? ['jest'] : [] }; } /** * Analiza configuración MCP */ analyzeMcpConfig() { return { generation: { strategy: 'modular', codeStyle: 'clean-architecture', patterns: ['repository', 'service', 'controller'], features: ['validation', 'error-handling', 'logging'] }, frontend: { framework: 'react', stateManagement: 'zustand', styling: 'tailwindcss', routing: 'react-router', forms: 'react-hook-form', validation: 'joi' }, sync: { autoUpdate: true, versionControl: 'git', deployOnSync: false } }; } /** * Genera checksums para integridad */ generateChecksums() { const contractString = JSON.stringify(this.contract, null, 2); const hash = crypto.createHash('sha256').update(contractString).digest('hex'); this.contract.contract.compatibility.contractHash = `sha256:${hash.substring(0, 16)}...`; this.contract.contract.compatibility.checksums = { entities: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.entities)).digest('hex').substring(0, 16)}...`, auth: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.auth)).digest('hex').substring(0, 16)}...`, modules: `sha256:${crypto.createHash('sha256').update(JSON.stringify(this.contract.modules)).digest('hex').substring(0, 16)}...` }; } // Métodos auxiliares loadEnvExample() { try { const envPath = path.join(this.projectRoot, '.env.example'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf8'); const env = {}; envContent.split('\n').forEach(line => { const [key, value] = line.split('='); if (key && value) { env[key.trim()] = value.trim().replace(/["']/g, ''); } }); return env; } } catch (error) { console.warn('Error cargando .env.example:', error.message); } return {}; } detectDatabaseType(env) { const dbUrl = env.DATABASE_URL || ''; if (dbUrl.includes('postgresql') || dbUrl.includes('postgres')) return 'postgresql'; if (dbUrl.includes('mysql')) return 'mysql'; if (dbUrl.includes('sqlite')) return 'sqlite'; if (dbUrl.includes('mongodb')) return 'mongodb'; return 'postgresql'; // default } detectMiddleware() { const middleware = ['helmet', 'compression', 'morgan', 'cors']; // Verificar si existen en package.json const deps = { ...this.packageJson.dependencies, ...this.packageJson.devDependencies }; if (deps['express-rate-limit']) middleware.push('express-rate-limit'); if (deps['express-validator']) middleware.push('express-validator'); return middleware; } extractAuthFeatures(content) { const features = []; if (content.includes('refresh')) features.push('refresh-tokens'); if (content.includes('email') && content.includes('verify')) features.push('email-verification'); if (content.includes('password') && content.includes('reset')) features.push('password-reset'); return features; } extractDatabaseProvider(content) { if (content.includes('postgresql') || content.includes('postgres')) return 'postgresql'; if (content.includes('mysql')) return 'mysql'; if (content.includes('sqlite')) return 'sqlite'; if (content.includes('mongodb')) return 'mongodb'; return 'postgresql'; } extractDatabaseFeatures(content) { const features = []; if (content.includes('migration')) features.push('migrations'); if (content.includes('seed')) features.push('seeders'); if (content.includes('soft') && content.includes('delete')) features.push('soft-deletes'); if (content.includes('audit')) features.push('audit-trail'); return features; } extractEmailProvider(content) { if (content.includes('resend')) return 'resend'; if (content.includes('sendgrid')) return 'sendgrid'; if (content.includes('mailgun')) return 'mailgun'; if (content.includes('ses')) return 'ses'; return 'smtp'; } extractEmailFeatures(content) { const features = []; if (content.includes('template')) features.push('templates'); if (content.includes('queue')) features.push('queue'); if (content.includes('track')) features.push('tracking'); return features; } capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Guarda el contrato en un archivo */ async saveContract(outputPath = null) { const contract = await this.generateContract(); const filePath = outputPath || path.join(this.projectRoot, 'frontend-contract.json'); fs.writeFileSync(filePath, JSON.stringify(contract, null, 2), 'utf8'); console.log(`✅ Contrato generado exitosamente: ${filePath}`); return filePath; } } // Ejecutar si se llama directamente if (require.main === module) { const generator = new ContractGenerator(); const outputPath = process.argv[2]; generator.saveContract(outputPath) .then(filePath => { console.log('🎉 Generación completada!'); console.log(`📄 Archivo: ${filePath}`); console.log('\n💡 Usa este contrato para generar automáticamente el frontend.'); }) .catch(error => { console.error('❌ Error generando contrato:', error); process.exit(1); }); } module.exports = ContractGenerator;