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.

628 lines (530 loc) 19.6 kB
const fs = require('fs'); const path = require('path'); const Handlebars = require('handlebars'); class ValidationModuleInitializer { constructor(projectPath, config = {}) { this.projectPath = projectPath; this.config = { strictMode: config.strictMode !== false, transform: config.transform !== false, whitelist: config.whitelist !== false, forbidNonWhitelisted: config.forbidNonWhitelisted !== false, skipMissingProperties: config.skipMissingProperties || false, alwaysTransform: config.alwaysTransform || false, enableCustomValidators: config.enableCustomValidators !== false, enableDatabaseValidation: config.enableDatabaseValidation !== false, enableFileValidation: config.enableFileValidation !== false, enableI18n: config.enableI18n || false, defaultLocale: config.defaultLocale || 'en', ...config }; this.modulePath = path.join(__dirname); } async initialize() { console.log('✅ Inicializando módulo de validación...'); try { // 1. Configurar estructura de directorios await this.setupDirectories(); // 2. Generar pipe de validación await this.generateValidationPipe(); // 3. Generar servicio de validación await this.generateValidationService(); // 4. Generar decorador de validación await this.generateValidateDecorator(); // 5. Generar esquemas de validación await this.generateValidationSchema(); // 6. Generar DTO base await this.generateBaseDto(); // 7. Generar validadores personalizados await this.generateCustomValidators(); // 8. Generar decoradores personalizados await this.generateCustomDecorators(); // 9. Generar interfaces await this.generateInterfaces(); // 10. Generar controlador (comentado - plantilla no disponible) // await this.generateController(); // 11. Generar módulo (comentado - plantilla no disponible) // await this.generateModule(); // 12. Actualizar package.json await this.updatePackageJson(); console.log('✅ Módulo de validación inicializado correctamente'); return { success: true, message: 'Validation module initialized successfully', files: this.getGeneratedFiles(), config: this.config }; } catch (error) { console.error('❌ Error inicializando módulo de validación:', error); throw error; } } async setupDirectories() { const dirs = [ 'src/validation', 'src/validation/validators', 'src/validation/decorators', 'src/validation/interfaces', 'src/validation/dto', 'src/validation/schemas', 'src/validation/pipes' ]; for (const dir of dirs) { const fullPath = path.join(this.projectPath, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } } } async generateValidationPipe() { const templatePath = path.join(this.modulePath, 'templates', 'validation.pipe.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/pipes/validation.pipe.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config, strictMode: this.config.strictMode, transform: this.config.transform, whitelist: this.config.whitelist, forbidNonWhitelisted: this.config.forbidNonWhitelisted }); fs.writeFileSync(outputPath, content); } async generateValidationService() { const templatePath = path.join(this.modulePath, 'templates', 'validation.service.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/validation.service.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config, enableDatabaseValidation: this.config.enableDatabaseValidation, enableI18n: this.config.enableI18n }); fs.writeFileSync(outputPath, content); } async generateValidateDecorator() { const templatePath = path.join(this.modulePath, 'templates', 'validate.decorator.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/decorators/validate.decorator.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config }); fs.writeFileSync(outputPath, content); } async generateValidationSchema() { const templatePath = path.join(this.modulePath, 'templates', 'validation.schema.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/schemas/validation.schema.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config }); fs.writeFileSync(outputPath, content); } async generateBaseDto() { const templatePath = path.join(this.modulePath, 'templates', 'base.dto.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/dto/base.dto.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config }); fs.writeFileSync(outputPath, content); } async generateCustomValidators() { const validatorsTemplate = `import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, } from 'class-validator'; import { Injectable } from '@nestjs/common'; {{#if enableDatabaseValidation}} import { PrismaService } from '../../database/prisma.service'; {{/if}} {{#if enableDatabaseValidation}} @ValidatorConstraint({ name: 'isUnique', async: true }) @Injectable() export class IsUniqueConstraint implements ValidatorConstraintInterface { constructor(private readonly prisma: PrismaService) {} async validate(value: any, args: ValidationArguments): Promise<boolean> { const [model, field] = args.constraints; const record = await this.prisma[model].findFirst({ where: { [field]: value }, }); return !record; } defaultMessage(args: ValidationArguments): string { const [model, field] = args.constraints; return \`\${field} already exists in \${model}\`; } } export function IsUnique( model: string, field: string, validationOptions?: ValidationOptions, ) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [model, field], validator: IsUniqueConstraint, }); }; } @ValidatorConstraint({ name: 'exists', async: true }) @Injectable() export class ExistsConstraint implements ValidatorConstraintInterface { constructor(private readonly prisma: PrismaService) {} async validate(value: any, args: ValidationArguments): Promise<boolean> { const [model, field] = args.constraints; const record = await this.prisma[model].findFirst({ where: { [field]: value }, }); return !!record; } defaultMessage(args: ValidationArguments): string { const [model, field] = args.constraints; return \`\${field} does not exist in \${model}\`; } } export function Exists( model: string, field: string = 'id', validationOptions?: ValidationOptions, ) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [model, field], validator: ExistsConstraint, }); }; } {{/if}} @ValidatorConstraint({ name: 'isStrongPassword', async: false }) export class IsStrongPasswordConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments): boolean { const minLength = args.constraints[0] || 8; const requireUppercase = args.constraints[1] !== false; const requireLowercase = args.constraints[2] !== false; const requireNumbers = args.constraints[3] !== false; const requireSymbols = args.constraints[4] !== false; if (password.length < minLength) return false; if (requireUppercase && !/[A-Z]/.test(password)) return false; if (requireLowercase && !/[a-z]/.test(password)) return false; if (requireNumbers && !/\\d/.test(password)) return false; if (requireSymbols && !/[^\\w\\s]/.test(password)) return false; return true; } defaultMessage(args: ValidationArguments): string { return 'Password is not strong enough'; } } export function IsStrongPassword( minLength = 8, requireUppercase = true, requireLowercase = true, requireNumbers = true, requireSymbols = true, validationOptions?: ValidationOptions, ) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [minLength, requireUppercase, requireLowercase, requireNumbers, requireSymbols], validator: IsStrongPasswordConstraint, }); }; } {{#if enableFileValidation}} @ValidatorConstraint({ name: 'isValidFile', async: false }) export class IsValidFileConstraint implements ValidatorConstraintInterface { validate(file: any, args: ValidationArguments): boolean { if (!file) return false; const allowedTypes = args.constraints[0] || []; const maxSize = args.constraints[1] || 5 * 1024 * 1024; // 5MB default if (allowedTypes.length > 0 && !allowedTypes.includes(file.mimetype)) { return false; } if (file.size > maxSize) { return false; } return true; } defaultMessage(args: ValidationArguments): string { return 'File is not valid'; } } export function IsValidFile( allowedTypes: string[] = [], maxSize: number = 5 * 1024 * 1024, validationOptions?: ValidationOptions, ) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [allowedTypes, maxSize], validator: IsValidFileConstraint, }); }; } {{/if}}`; const outputPath = path.join(this.projectPath, 'src/validation/validators/custom.validators.ts'); const compiledTemplate = Handlebars.compile(validatorsTemplate); const content = compiledTemplate({ config: this.config, enableDatabaseValidation: this.config.enableDatabaseValidation, enableFileValidation: this.config.enableFileValidation }); fs.writeFileSync(outputPath, content); } async generateCustomDecorators() { const decoratorsTemplate = `import { Transform } from 'class-transformer'; import { applyDecorators } from '@nestjs/common'; import { IsString, IsNotEmpty, IsEmail, IsOptional, Length, Matches, ValidationOptions, } from 'class-validator'; // Decorador para emails export function IsEmailField(validationOptions?: ValidationOptions) { return applyDecorators( IsString(validationOptions), IsEmail({}, validationOptions), Transform(({ value }) => value?.toLowerCase().trim()), ); } // Decorador para contraseñas export function IsPasswordField( minLength = 8, validationOptions?: ValidationOptions, ) { return applyDecorators( IsString(validationOptions), IsNotEmpty(validationOptions), Length(minLength, 128, validationOptions), ); } // Decorador para nombres export function IsNameField( minLength = 2, maxLength = 50, validationOptions?: ValidationOptions, ) { return applyDecorators( IsString(validationOptions), IsNotEmpty(validationOptions), Length(minLength, maxLength, validationOptions), Transform(({ value }) => value?.trim()), Matches(/^[a-zA-ZÀ-ÿ\\s]+$/, { message: 'Name can only contain letters and spaces', ...validationOptions, }), ); } // Decorador para teléfonos export function IsPhoneField(validationOptions?: ValidationOptions) { return applyDecorators( IsString(validationOptions), Matches(/^\\+?[1-9]\\d{1,14}$/, { message: 'Phone number must be valid', ...validationOptions, }), Transform(({ value }) => value?.replace(/\\s+/g, '')), ); } // Decorador para URLs export function IsUrlField(validationOptions?: ValidationOptions) { return applyDecorators( IsString(validationOptions), Matches(/^https?:\/\/.+/, { message: 'URL must be valid', ...validationOptions, }), ); } // Decorador para campos opcionales de texto export function IsOptionalText( minLength = 0, maxLength = 255, validationOptions?: ValidationOptions, ) { return applyDecorators( IsOptional(), IsString(validationOptions), Length(minLength, maxLength, validationOptions), Transform(({ value }) => value?.trim() || undefined), ); } // Decorador para slugs export function IsSlugField(validationOptions?: ValidationOptions) { return applyDecorators( IsString(validationOptions), IsNotEmpty(validationOptions), Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: 'Slug must be lowercase letters, numbers and hyphens only', ...validationOptions, }), ); }`; const outputPath = path.join(this.projectPath, 'src/validation/decorators/validation.decorators.ts'); fs.writeFileSync(outputPath, decoratorsTemplate); } async generateInterfaces() { const interfacesTemplate = `export interface ValidationResult { isValid: boolean; errors: ValidationError[]; data?: any; } export interface ValidationError { field: string; value: any; constraints: Record<string, string>; children?: ValidationError[]; } export interface ValidationOptions { groups?: string[]; skipMissingProperties?: boolean; whitelist?: boolean; forbidNonWhitelisted?: boolean; transform?: boolean; strictGroups?: boolean; } export interface CustomValidatorOptions { async?: boolean; message?: string | ((args: any) => string); groups?: string[]; } export interface ValidationRule { field: string; rules: string[]; message?: string; conditions?: Record<string, any>; } export interface ValidationSchema { name: string; version: string; rules: ValidationRule[]; groups?: string[]; metadata?: Record<string, any>; }`; const outputPath = path.join(this.projectPath, 'src/validation/interfaces/validation.interface.ts'); fs.writeFileSync(outputPath, interfacesTemplate); } async generateController() { const templatePath = path.join(this.modulePath, 'templates', 'validation.controller.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/validation.controller.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config }); fs.writeFileSync(outputPath, content); } async generateModule() { const templatePath = path.join(this.modulePath, 'templates', 'validation.module.ts.hbs'); const outputPath = path.join(this.projectPath, 'src/validation/validation.module.ts'); const template = fs.readFileSync(templatePath, 'utf8'); const compiledTemplate = Handlebars.compile(template); const content = compiledTemplate({ config: this.config, enableDatabaseValidation: this.config.enableDatabaseValidation, enableCustomValidators: this.config.enableCustomValidators }); fs.writeFileSync(outputPath, content); } async updatePackageJson() { const packageJsonPath = path.join(this.projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { console.log('⚠️ package.json no encontrado'); return; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const dependencies = { 'class-validator': '^0.14.0', 'class-transformer': '^0.5.1' }; // Agregar dependencias opcionales if (this.config.enableI18n) { dependencies['nestjs-i18n'] = '^10.4.0'; } packageJson.dependencies = { ...packageJson.dependencies, ...dependencies }; // Agregar scripts de validación packageJson.scripts = { ...packageJson.scripts, 'validation:generate': 'node scripts/generate-dto.js', 'validation:test': 'node scripts/test-validation.js', 'validation:schemas': 'node scripts/validate-schemas.js' }; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log('📦 package.json actualizado con dependencias de validación'); } getGeneratedFiles() { return [ 'src/validation/pipes/validation.pipe.ts', 'src/validation/validation.service.ts', 'src/validation/decorators/validate.decorator.ts', 'src/validation/schemas/validation.schema.ts', 'src/validation/dto/base.dto.ts', 'src/validation/validators/custom.validators.ts', 'src/validation/decorators/validation.decorators.ts', 'src/validation/interfaces/validation.interface.ts' // 'src/validation/validation.controller.ts', // Comentado - plantilla no disponible // 'src/validation/validation.module.ts' // Comentado - plantilla no disponible ]; } } module.exports = ValidationModuleInitializer; // CLI usage if (require.main === module) { const projectPath = process.argv[2] || process.cwd(); const config = { strictMode: !process.argv.includes('--no-strict'), transform: !process.argv.includes('--no-transform'), enableDatabaseValidation: process.argv.includes('--database'), enableFileValidation: process.argv.includes('--files'), enableI18n: process.argv.includes('--i18n') }; const initializer = new ValidationModuleInitializer(projectPath, config); initializer.initialize() .then(result => { console.log('\n🎉 Módulo de validación configurado exitosamente!'); console.log('\n📁 Archivos generados:'); result.files.forEach(file => console.log(` - ${file}`)); console.log('\n⚙️ Configuración aplicada:'); console.log(` - Strict Mode: ${result.config.strictMode}`); console.log(` - Transform: ${result.config.transform}`); console.log(` - Whitelist: ${result.config.whitelist}`); console.log(` - Database Validation: ${result.config.enableDatabaseValidation}`); console.log(` - File Validation: ${result.config.enableFileValidation}`); console.log(` - I18n: ${result.config.enableI18n}`); console.log('\n🚀 Próximos pasos:'); console.log(' 1. Ejecutar: npm install'); console.log(' 2. Importar ValidationModule en tu AppModule'); console.log(' 3. Configurar ValidationPipe globalmente'); console.log(' 4. Crear DTOs con decoradores de validación'); console.log(' 5. Usar validadores personalizados según necesidad'); }) .catch(error => { console.error('❌ Error:', error.message); process.exit(1); }); }