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.

444 lines (376 loc) 14.2 kB
/** * CRUD Module Initialization Script * Genera operaciones CRUD completas para entidades */ const fs = require('fs'); const path = require('path'); const Handlebars = require('handlebars'); class CrudModuleInit { constructor() { this.templatesPath = path.join(__dirname, 'templates'); this.generatedFiles = []; } /** * Ejecuta la inicialización del módulo CRUD * @param {Object} context - Contexto de ejecución */ async execute(context) { const { outputPath, modulePath, options, manifest } = context; // Almacenar la configuración para uso en otros métodos this.config = options; console.log('🔍 CRUD Module - Configuración recibida:'); console.log(' - options.database:', JSON.stringify(options.database, null, 2)); console.log(' - options.projectConfig:', JSON.stringify(options.projectConfig?.database, null, 2)); console.log(' - Tablas en database:', options.database?.tables?.length || 0); console.log(' - Tablas en projectConfig:', options.projectConfig?.database?.tables?.length || 0); console.log('🔧 Inicializando módulo CRUD...'); try { // 1. Detectar entidades del esquema Prisma const entities = await this.detectEntities(outputPath); console.log(`📋 Entidades detectadas: ${entities.join(', ')}`); // 2. Configurar estructura de directorios await this.setupDirectories(outputPath); // 3. Generar archivos base del CRUD await this.generateBaseFiles(outputPath, modulePath, options); // 4. Generar archivos específicos por entidad for (const entity of entities) { await this.generateEntityFiles(entity, outputPath, modulePath, options); } // 5. Configurar rutas principales await this.setupMainRoutes(outputPath, entities, options); // 6. Actualizar package.json con dependencias await this.updatePackageJson(outputPath); console.log('✅ Módulo CRUD inicializado exitosamente'); return { success: true, filesGenerated: this.generatedFiles.length, entities: entities, metadata: { module: 'crud', version: manifest.version, features: manifest.features } }; } catch (error) { console.error('❌ Error inicializando módulo CRUD:', error.message); throw error; } } /** * Detecta entidades del esquema Prisma y configuración * @param {string} outputPath - Ruta de salida * @returns {Array} Lista de entidades */ async detectEntities(outputPath) { const entities = []; console.log('🔍 Detectando entidades...'); // 1. Primero, obtener entidades de la configuración de tablas definidas // Priorizar projectConfig si está disponible let tablesConfig = null; if (this.config && this.config.projectConfig && this.config.projectConfig.database && this.config.projectConfig.database.tables) { tablesConfig = this.config.projectConfig.database.tables; console.log(' - Usando tablas de projectConfig:', tablesConfig.length); } else if (this.config && this.config.database && this.config.database.tables) { tablesConfig = this.config.database.tables; console.log(' - Usando tablas de database config:', tablesConfig.length); } if (tablesConfig && tablesConfig.length > 0) { for (const table of tablesConfig) { // Obtener el nombre de la tabla (puede ser 'name' o 'tableName') const tableName = table.tableName || table.name; // Validar que la tabla tenga un nombre válido if (!table || !tableName || typeof tableName !== 'string') { console.log(` - Tabla inválida encontrada:`, table); continue; } // Convertir nombre de tabla a PascalCase para el modelo const entityName = this.toPascalCase(tableName); entities.push(entityName); console.log(` - Entidad detectada: ${tableName} -> ${entityName}`); } } // 2. Si no hay tablas definidas, leer del schema.prisma if (entities.length === 0) { const schemaPath = path.join(outputPath, 'prisma', 'schema.prisma'); if (fs.existsSync(schemaPath)) { const schemaContent = fs.readFileSync(schemaPath, 'utf8'); // Extraer modelos del schema const modelRegex = /model\s+(\w+)\s*{/g; let match; while ((match = modelRegex.exec(schemaContent)) !== null) { const modelName = match[1]; // Excluir modelos del sistema if (!['AuditLog', 'HealthCheck', 'Session'].includes(modelName)) { entities.push(modelName); } } } } // Si no hay entidades, crear una de ejemplo if (entities.length === 0) { entities.push('User'); } return entities; } /** * Convierte un string a PascalCase * @param {string} str - String a convertir * @returns {string} String en PascalCase */ toPascalCase(str) { return str .split(/[_-\s]+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } /** * Configura la estructura de directorios * @param {string} outputPath - Ruta de salida */ async setupDirectories(outputPath) { const directories = [ 'src/controllers', 'src/services', 'src/dto', 'src/routes', 'src/middleware', 'src/validators', 'src/types' ]; for (const dir of directories) { const fullPath = path.join(outputPath, dir); fs.mkdirSync(fullPath, { recursive: true }); } } /** * Genera archivos base del CRUD * @param {string} outputPath - Ruta de salida * @param {string} modulePath - Ruta del módulo * @param {Object} options - Opciones */ async generateBaseFiles(outputPath, modulePath, options) { // Tipos base para CRUD const crudTypesContent = ` export interface PaginationParams { page?: number; limit?: number; search?: string; sort?: string; } export interface PaginationResult<T> { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasNext: boolean; hasPrev: boolean; }; } export interface CrudService<T> { findAll(params: PaginationParams): Promise<PaginationResult<T>>; findById(id: string): Promise<T | null>; create(data: Partial<T>): Promise<T>; update(id: string, data: Partial<T>): Promise<T>; delete(id: string): Promise<void>; } export interface FilterOptions { where?: any; orderBy?: any; include?: any; } `; const typesPath = path.join(outputPath, 'src/types/crud.types.ts'); fs.writeFileSync(typesPath, crudTypesContent); this.generatedFiles.push(typesPath); // Middleware base para CRUD const middlewareContent = ` import { Request, Response, NextFunction } from 'express'; import { PaginationParams } from '../types/crud.types'; /** * Middleware para parsear parámetros de paginación */ export const paginationMiddleware = ( req: Request, res: Response, next: NextFunction ) => { const page = parseInt(req.query.page as string) || 1; const limit = Math.min( parseInt(req.query.limit as string) || 10, parseInt(process.env.CRUD_MAX_PAGE_SIZE || '100') ); const search = req.query.search as string; const sort = req.query.sort as string; req.pagination = { page: Math.max(1, page), limit: Math.max(1, limit), search, sort } as PaginationParams; next(); }; /** * Middleware para validar IDs */ export const validateIdMiddleware = ( req: Request, res: Response, next: NextFunction ) => { const { id } = req.params; if (!id || id.trim() === '') { return res.status(400).json({ error: 'ID is required' }); } next(); }; /** * Middleware para sanitizar input */ export const sanitizeInputMiddleware = ( req: Request, res: Response, next: NextFunction ) => { // Remover campos del sistema const systemFields = ['id', 'createdAt', 'updatedAt', 'deletedAt']; if (req.body) { systemFields.forEach(field => { delete req.body[field]; }); } next(); }; `; const middlewarePath = path.join(outputPath, 'src/middleware/crud.middleware.ts'); fs.writeFileSync(middlewarePath, middlewareContent); this.generatedFiles.push(middlewarePath); } /** * Genera archivos específicos por entidad * @param {string} entity - Nombre de la entidad * @param {string} outputPath - Ruta de salida * @param {string} modulePath - Ruta del módulo * @param {Object} options - Opciones */ async generateEntityFiles(entity, outputPath, modulePath, options) { const entityLower = entity.toLowerCase(); const entityPlural = entityLower + 's'; // Simplificado // Generar controlador await this.generateFromTemplate( 'controller.ts.hbs', path.join(outputPath, `src/controllers/${entityLower}.controller.ts`), modulePath, { entity, entityLower, entityPlural, ...options } ); // Generar servicio await this.generateFromTemplate( 'service.ts.hbs', path.join(outputPath, `src/services/${entityLower}.service.ts`), modulePath, { entity, entityLower, entityPlural, ...options } ); // Generar DTOs await this.generateFromTemplate( 'dto.ts.hbs', path.join(outputPath, `src/dto/${entityLower}.dto.ts`), modulePath, { entity, entityLower, entityPlural, ...options } ); // Generar rutas await this.generateFromTemplate( 'routes.ts.hbs', path.join(outputPath, `src/routes/${entityLower}.routes.ts`), modulePath, { entity, entityLower, entityPlural, ...options } ); // Generar validador await this.generateFromTemplate( 'validator.ts.hbs', path.join(outputPath, `src/validators/${entityLower}.validator.ts`), modulePath, { entity, entityLower, entityPlural, ...options } ); } /** * Configura las rutas principales * @param {string} outputPath - Ruta de salida * @param {Array} entities - Lista de entidades * @param {Object} options - Opciones */ async setupMainRoutes(outputPath, entities, options) { const routesContent = ` import { Router } from 'express'; ${entities.map(entity => `import ${entity.toLowerCase()}Routes from './${entity.toLowerCase()}.routes';` ).join('\n')} const router = Router(); // Registrar rutas de entidades ${entities.map(entity => `router.use('/${entity.toLowerCase()}s', ${entity.toLowerCase()}Routes);` ).join('\n')} // Ruta de salud para CRUD router.get('/health', (req, res) => { res.json({ status: 'ok', module: 'crud', entities: [${entities.map(e => `'${e.toLowerCase()}'`).join(', ')}], timestamp: new Date().toISOString() }); }); export default router; `; const mainRoutesPath = path.join(outputPath, 'src/routes/index.ts'); fs.writeFileSync(mainRoutesPath, routesContent); this.generatedFiles.push(mainRoutesPath); } /** * Genera archivo desde template * @param {string} templateName - Nombre del template * @param {string} outputFile - Archivo de salida * @param {string} modulePath - Ruta del módulo * @param {Object} context - Contexto para el template */ async generateFromTemplate(templateName, outputFile, modulePath, context) { const templatePath = path.join(modulePath, 'templates', templateName); if (fs.existsSync(templatePath)) { const templateContent = fs.readFileSync(templatePath, 'utf8'); const template = Handlebars.compile(templateContent); const result = template(context); fs.writeFileSync(outputFile, result); this.generatedFiles.push(outputFile); } } /** * Actualiza package.json con dependencias del CRUD * @param {string} outputPath - Ruta de salida */ async updatePackageJson(outputPath) { const packagePath = path.join(outputPath, 'package.json'); let packageJson = {}; if (fs.existsSync(packagePath)) { packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); } // Dependencias para CRUD const dependencies = { 'express': '^4.18.2', 'express-validator': '^7.0.1', 'class-transformer': '^0.5.1', 'class-validator': '^0.14.0' }; packageJson.dependencies = { ...packageJson.dependencies, ...dependencies }; fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); } } module.exports = { execute: async (context) => { const init = new CrudModuleInit(); return await init.execute(context); } };