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.

563 lines (470 loc) 17.9 kB
// modules/database/init.js const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); class DatabaseModuleInitializer { constructor(config = {}) { // Si se pasa projectConfig, usar las tablas de ahí const projectConfig = config.projectConfig || {}; const databaseConfig = projectConfig.database || config.database || {}; // Inicializar config primero sin el provider this.config = { projectPath: config.projectPath || process.cwd(), features: config.features || ['prisma-orm', 'health-checks', 'migrations'], poolSize: config.poolSize || 10, enableLogging: config.enableLogging || false, database: databaseConfig, ...config }; // Ahora detectar el provider después de que this.config esté inicializado this.config.provider = config.provider || this.detectDatabaseProvider(config); // Log para debug console.log('🔍 DatabaseModuleInitializer configurado con:'); console.log(' - Provider:', this.config.provider); console.log(' - Database config:', JSON.stringify(this.config.database, null, 2)); console.log(' - Tables found:', this.config.database?.tables?.length || 0); this.metadata = { module: 'database', version: '1.0.0', generatedFiles: [], dependencies: [], environment: [], provider: this.config.provider }; } async initialize() { console.log('🗄️ Inicializando módulo de base de datos...'); try { // 1. Detectar proveedor de base de datos await this.detectDatabaseProvider(); // 2. Configurar Prisma await this.setupPrismaConfiguration(); // 3. Crear schema base await this.createBaseSchema(); // 4. Configurar pool de conexiones await this.configureConnectionPool(); // 5. Configurar health checks await this.setupHealthChecks(); // 6. Crear archivos de seeding await this.createSeederFiles(); // 7. Retornar metadata return this.returnDatabaseMetadata(); } catch (error) { console.error('❌ Error inicializando módulo database:', error.message); throw error; } } detectDatabaseProvider(config = {}) { // Detectar desde variables de entorno if (process.env.DATABASE_URL) { if (process.env.DATABASE_URL.includes('postgresql')) return 'postgresql'; if (process.env.DATABASE_URL.includes('mysql')) return 'mysql'; if (process.env.DATABASE_URL.includes('mongodb')) return 'mongodb'; if (process.env.DATABASE_URL.includes('sqlite') || process.env.DATABASE_URL.includes('file:')) return 'sqlite'; } // Detectar desde configuración if (config.database) { const db = config.database.toLowerCase(); if (db.includes('postgres')) return 'postgresql'; if (db.includes('mysql')) return 'mysql'; if (db.includes('mongo')) return 'mongodb'; if (db.includes('sqlite')) return 'sqlite'; } // Detectar desde dependencias existentes const projectPath = config.projectPath || this.config?.projectPath || process.cwd(); const packageJsonPath = path.join(projectPath, 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (deps.pg || deps['@types/pg']) return 'postgresql'; if (deps.mysql2 || deps.mysql) return 'mysql'; if (deps.mongodb || deps.mongoose) return 'mongodb'; if (deps.sqlite3 || deps['better-sqlite3']) return 'sqlite'; } // Por defecto PostgreSQL para aplicaciones empresariales return 'postgresql'; } async setupPrismaConfiguration() { console.log('⚙️ Configurando Prisma...'); // Crear directorio prisma si no existe const prismaDir = path.join(this.config.projectPath, 'prisma'); if (!fs.existsSync(prismaDir)) { fs.mkdirSync(prismaDir, { recursive: true }); } // Configurar variables de entorno await this.setupEnvironmentVariables(); // Instalar dependencias de Prisma si no están instaladas await this.installPrismaDependencies(); this.metadata.generatedFiles.push('prisma/schema.prisma'); } async setupEnvironmentVariables() { const envPath = path.join(this.config.projectPath, '.env'); const envExamplePath = path.join(this.config.projectPath, '.env.example'); // Obtener configuración de base de datos const databaseConfig = this.getDatabaseConfig(); // Usar DATABASE_URL del agente si está disponible let databaseUrl; if (process.env.DATABASE_URL && this.config.configuredFromAgent) { console.log('🔗 Usando DATABASE_URL configurada desde el agente'); databaseUrl = process.env.DATABASE_URL; } else { databaseUrl = databaseConfig.url; } const envContent = [ '# Database Configuration', `DATABASE_URL="${databaseUrl}"`, `DATABASE_PROVIDER="${this.config.provider}"`, `DATABASE_POOL_SIZE=${this.config.poolSize}`, `DATABASE_TIMEOUT=5000`, `DATABASE_SSL=false`, `DATABASE_LOGGING=${this.config.enableLogging}`, '' ].join('\n'); // Agregar a .env si no existe if (!fs.existsSync(envPath)) { fs.writeFileSync(envPath, envContent); } else { const existingEnv = fs.readFileSync(envPath, 'utf8'); if (!existingEnv.includes('DATABASE_URL')) { fs.appendFileSync(envPath, '\n' + envContent); } } // Agregar a .env.example const exampleContent = envContent.replace( databaseConfig.url, databaseConfig.exampleUrl ); if (!fs.existsSync(envExamplePath)) { fs.writeFileSync(envExamplePath, exampleContent); } else { const existingExample = fs.readFileSync(envExamplePath, 'utf8'); if (!existingExample.includes('DATABASE_URL')) { fs.appendFileSync(envExamplePath, '\n' + exampleContent); } } this.metadata.environment.push( 'DATABASE_URL', 'DATABASE_PROVIDER', 'DATABASE_POOL_SIZE', 'DATABASE_TIMEOUT', 'DATABASE_SSL', 'DATABASE_LOGGING' ); } getDatabaseConfig() { const configs = { postgresql: { url: 'postgresql://username:password@localhost:5432/database_name?schema=public', exampleUrl: 'postgresql://username:password@localhost:5432/database_name?schema=public' }, mysql: { url: 'mysql://username:password@localhost:3306/database_name', exampleUrl: 'mysql://username:password@localhost:3306/database_name' }, mongodb: { url: 'mongodb://username:password@localhost:27017/database_name', exampleUrl: 'mongodb://username:password@localhost:27017/database_name' }, sqlite: { url: 'file:./dev.db', exampleUrl: 'file:./dev.db' } }; return configs[this.config.provider] || configs.postgresql; } async installPrismaDependencies() { const packageJsonPath = path.join(this.config.projectPath, 'package.json'); let packageJson; if (!fs.existsSync(packageJsonPath)) { console.log('📦 Creando package.json...'); packageJson = { name: 'backend-api', version: '1.0.0', description: 'Backend API generated by MCP', main: 'src/index.ts', scripts: { 'dev': 'tsx watch src/index.ts', 'build': 'tsc', 'start': 'node dist/index.js', 'db:generate': 'prisma generate', 'db:push': 'prisma db push', 'db:migrate': 'prisma migrate dev', 'db:deploy': 'prisma migrate deploy', 'db:seed': 'tsx prisma/seed.ts' }, dependencies: {}, devDependencies: {} }; } else { packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } const dependencies = ['@prisma/client']; const devDependencies = ['prisma']; const providerDeps = { postgresql: ['pg', '@types/pg'], mysql: ['mysql2'], mongodb: [], sqlite: [] }; // Add provider-specific dependencies const providerSpecific = providerDeps[this.config.provider] || []; dependencies.push(...providerSpecific.filter(dep => !dep.startsWith('@types'))); devDependencies.push(...providerSpecific.filter(dep => dep.startsWith('@types'))); // Ensure dependencies and devDependencies objects exist if (!packageJson.dependencies) packageJson.dependencies = {}; if (!packageJson.devDependencies) packageJson.devDependencies = {}; // Add dependencies to package.json dependencies.forEach(dep => { packageJson.dependencies[dep] = 'latest'; }); devDependencies.forEach(dep => { packageJson.devDependencies[dep] = 'latest'; }); // Write updated package.json fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); this.metadata.dependencies = [...dependencies, ...devDependencies]; } async createBaseSchema() { console.log('📋 Creando schema base...'); const schemaPath = path.join(this.config.projectPath, 'prisma', 'schema.prisma'); const schema = this.generatePrismaSchema(); fs.writeFileSync(schemaPath, schema); this.metadata.generatedFiles.push('prisma/schema.prisma'); } generatePrismaSchema() { const providerConfig = { postgresql: 'postgresql', mysql: 'mysql', mongodb: 'mongodb', sqlite: 'sqlite' }; let schema = `// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" previewFeatures = ["fullTextSearch", "metrics"] } datasource db { provider = "${providerConfig[this.config.provider]}" url = env("DATABASE_URL") } `; // Add user-defined tables if they exist if (this.config.database && this.config.database.tables && this.config.database.tables.length > 0) { console.log('📋 Generando modelos desde tablas definidas:', this.config.database.tables.map(t => t.tableName)); for (const table of this.config.database.tables) { schema += this.generateModelFromTable(table); } } else { console.log('📋 No se encontraron tablas definidas, usando modelos por defecto'); // Default models if no tables are defined schema += `// User model - base entity for most applications model User { id String @id @default(cuid()) email String @unique password String name String? role String @default("user") isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") } `; } // Always add audit and health check models schema += `// Base model with common fields model AuditLog { id String @id @default(cuid()) userId String? action String entityType String? entityId String? details Json? ipAddress String? userAgent String? createdAt DateTime @default(now()) @@map("audit_logs") } // Health check model for database monitoring model HealthCheck { id String @id @default(cuid()) service String status String details Json? checkedAt DateTime @default(now()) @@map("health_checks") } `; return schema; } generateModelFromTable(table) { const { tableName, fields, relations = [] } = table; // Capitalize first letter for model name const modelName = tableName.charAt(0).toUpperCase() + tableName.slice(1); let model = `// ${tableName} model model ${modelName} { `; // Add all fields from the table definition for (const field of fields) { const prismaType = this.mapToPrismaType(field.type); const isRequired = field.required ? '' : '?'; const isUnique = field.unique ? ' @unique' : ''; const isId = field.name === 'id' ? (field.type === 'String' ? ' @id @default(cuid())' : ' @id @default(autoincrement())') : ''; const defaultValue = field.defaultValue && field.name !== 'id' ? ` @default(${this.formatDefaultValue(field.defaultValue, field.type)})` : ''; const autoTimestamp = field.name === 'createdAt' ? ' @default(now())' : field.name === 'updatedAt' ? ' @updatedAt' : ''; model += ` ${field.name.padEnd(12)} ${prismaType}${isRequired}${isId}${isUnique}${defaultValue}${autoTimestamp} `; } // Add relations if any if (relations && relations.length > 0) { model += ` // Relations `; for (const relation of relations) { if (relation.type === 'manyToOne' && relation.foreignKey) { // Many-to-one relation (this model has foreign key) const relatedModelName = relation.relatedTable.charAt(0).toUpperCase() + relation.relatedTable.slice(1); const relationName = relation.foreignKey.replace('Id', '').replace('_id', ''); model += ` ${relationName.padEnd(12)} ${relatedModelName}? @relation(fields: [${relation.foreignKey}], references: [id]) `; } else if (relation.type === 'oneToMany') { // One-to-many relation (this model is referenced by foreign key) const relatedModelName = relation.relatedTable.charAt(0).toUpperCase() + relation.relatedTable.slice(1); const relationName = relation.relatedTable.toLowerCase() + 's'; model += ` ${relationName.padEnd(12)} ${relatedModelName}[] `; } } } // Add table mapping model += ` @@map("${tableName}") `; model += `} `; return model; } formatDefaultValue(value, type) { if (type === 'String') { return `"${value}"`; } if (type === 'Boolean') { return value.toString().toLowerCase(); } return value; } mapToPrismaType(type) { const typeMapping = { 'String': 'String', 'Int': 'Int', 'Float': 'Float', 'Boolean': 'Boolean', 'DateTime': 'DateTime', 'Json': 'Json' }; return typeMapping[type] || 'String'; } async configureConnectionPool() { console.log('🔗 Configurando pool de conexiones...'); const connectionPath = path.join(this.config.projectPath, 'src', 'database'); if (!fs.existsSync(connectionPath)) { fs.mkdirSync(connectionPath, { recursive: true }); } this.metadata.generatedFiles.push( 'src/database/connection.ts', 'src/database/database.config.ts' ); } async setupHealthChecks() { console.log('🏥 Configurando health checks...'); const healthPath = path.join(this.config.projectPath, 'src', 'health'); if (!fs.existsSync(healthPath)) { fs.mkdirSync(healthPath, { recursive: true }); } this.metadata.generatedFiles.push( 'src/health/database.health.ts', 'src/health/health.controller.ts' ); } async createSeederFiles() { console.log('🌱 Creando archivos de seeding...'); const seederPath = path.join(this.config.projectPath, 'prisma', 'seed.ts'); const seederContent = `import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { console.log('🌱 Seeding database...'); // Add your seed data here console.log('✅ Database seeded successfully'); } main() .catch((e) => { console.error('❌ Error seeding database:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); }); `; fs.writeFileSync(seederPath, seederContent); this.metadata.generatedFiles.push('prisma/seed.ts'); } returnDatabaseMetadata() { console.log('✅ Módulo de base de datos inicializado correctamente'); return { ...this.metadata, config: { provider: this.config.provider, features: this.config.features, poolSize: this.config.poolSize, loggingEnabled: this.config.enableLogging }, instructions: { nextSteps: [ 'Instalar dependencias: npm install prisma @prisma/client', 'Generar cliente Prisma: npx prisma generate', 'Aplicar schema: npx prisma db push', 'Ejecutar seeds: npm run db:seed' ], development: [ 'Usar prisma studio: npx prisma studio', 'Ver logs de consultas en desarrollo', 'Configurar backup automático' ], production: [ 'Usar migraciones: npx prisma migrate deploy', 'Configurar SSL para la conexión', 'Monitorear métricas de rendimiento', 'Implementar estrategia de backup' ] }, endpoints: [ { method: 'GET', path: '/health/database', description: 'Database health check' }, { method: 'GET', path: '/admin/database/stats', description: 'Database statistics' } ] }; } } // Función principal para uso con agentes IA async function initializeDatabaseModule(config = {}) { const initializer = new DatabaseModuleInitializer(config); return await initializer.initialize(); } // Exportar para uso en otros módulos module.exports = { DatabaseModuleInitializer, initializeDatabaseModule }; // Si se ejecuta directamente if (require.main === module) { const config = { projectPath: process.argv[2] || process.cwd(), provider: process.argv[3] || undefined, features: process.argv[4] ? process.argv[4].split(',') : undefined }; initializeDatabaseModule(config) .then(result => { console.log('\n📊 Resultado de inicialización:'); console.log(JSON.stringify(result, null, 2)); }) .catch(error => { console.error('❌ Error:', error.message); process.exit(1); }); }