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
JavaScript
/**
* 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);
}
};