Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
620 lines
15 KiB
Markdown
620 lines
15 KiB
Markdown
# PATRÓN: VALIDACIÓN DE DATOS
|
|
|
|
**Versión:** 1.0.0
|
|
**Fecha:** 2025-12-08
|
|
**Aplica a:** Backend (NestJS/Express), Frontend (React)
|
|
**Prioridad:** OBLIGATORIA
|
|
|
|
---
|
|
|
|
## PROPÓSITO
|
|
|
|
Definir patrones estándar de validación de datos para garantizar consistencia en toda la aplicación.
|
|
|
|
---
|
|
|
|
## PRINCIPIO FUNDAMENTAL
|
|
|
|
```
|
|
╔══════════════════════════════════════════════════════════════════════╗
|
|
║ VALIDACIÓN EN CAPAS ║
|
|
║ ║
|
|
║ 1. Frontend: UX inmediata (opcional pero recomendada) ║
|
|
║ 2. DTO: Validación de entrada (OBLIGATORIA) ║
|
|
║ 3. Service: Validación de negocio (cuando aplica) ║
|
|
║ 4. Database: Constraints (última línea de defensa) ║
|
|
╚══════════════════════════════════════════════════════════════════════╝
|
|
```
|
|
|
|
---
|
|
|
|
## 1. VALIDACIÓN EN DTO (NestJS - class-validator)
|
|
|
|
### Decoradores Básicos
|
|
|
|
```typescript
|
|
import {
|
|
IsString,
|
|
IsEmail,
|
|
IsNotEmpty,
|
|
IsOptional,
|
|
IsUUID,
|
|
IsInt,
|
|
IsNumber,
|
|
IsBoolean,
|
|
IsDate,
|
|
IsArray,
|
|
IsEnum,
|
|
IsUrl,
|
|
IsPhoneNumber,
|
|
} from 'class-validator';
|
|
```
|
|
|
|
### Decoradores de Longitud
|
|
|
|
```typescript
|
|
import {
|
|
Length,
|
|
MinLength,
|
|
MaxLength,
|
|
Min,
|
|
Max,
|
|
ArrayMinSize,
|
|
ArrayMaxSize,
|
|
} from 'class-validator';
|
|
```
|
|
|
|
### Decoradores de Formato
|
|
|
|
```typescript
|
|
import {
|
|
Matches,
|
|
IsAlpha,
|
|
IsAlphanumeric,
|
|
IsAscii,
|
|
Contains,
|
|
IsISO8601,
|
|
IsCreditCard,
|
|
IsHexColor,
|
|
IsJSON,
|
|
} from 'class-validator';
|
|
```
|
|
|
|
---
|
|
|
|
## 2. PATRONES POR TIPO DE DATO
|
|
|
|
### Email
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
@ApiProperty({
|
|
description: 'Correo electrónico del usuario',
|
|
example: 'usuario@empresa.com',
|
|
})
|
|
@IsEmail({}, { message: 'El correo electrónico no es válido' })
|
|
@IsNotEmpty({ message: 'El correo electrónico es requerido' })
|
|
@MaxLength(255, { message: 'El correo no puede exceder 255 caracteres' })
|
|
@Transform(({ value }) => value?.toLowerCase().trim())
|
|
email: string;
|
|
```
|
|
|
|
### Password
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Contraseña segura
|
|
@ApiProperty({
|
|
description: 'Contraseña (mín 8 caracteres, 1 mayúscula, 1 número, 1 especial)',
|
|
example: 'MiPassword123!',
|
|
})
|
|
@IsString()
|
|
@IsNotEmpty({ message: 'La contraseña es requerida' })
|
|
@MinLength(8, { message: 'La contraseña debe tener al menos 8 caracteres' })
|
|
@MaxLength(100, { message: 'La contraseña no puede exceder 100 caracteres' })
|
|
@Matches(
|
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
|
|
{ message: 'La contraseña debe contener mayúscula, minúscula, número y carácter especial' }
|
|
)
|
|
password: string;
|
|
```
|
|
|
|
### UUID
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
@ApiProperty({
|
|
description: 'ID único del recurso',
|
|
example: '550e8400-e29b-41d4-a716-446655440000',
|
|
})
|
|
@IsUUID('4', { message: 'El ID debe ser un UUID válido' })
|
|
@IsNotEmpty({ message: 'El ID es requerido' })
|
|
id: string;
|
|
|
|
// UUID Opcional (para referencias)
|
|
@ApiPropertyOptional({
|
|
description: 'ID del usuario relacionado',
|
|
})
|
|
@IsOptional()
|
|
@IsUUID('4', { message: 'El ID de usuario debe ser un UUID válido' })
|
|
userId?: string;
|
|
```
|
|
|
|
### Nombre/Texto Simple
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
@ApiProperty({
|
|
description: 'Nombre del usuario',
|
|
example: 'Juan Pérez',
|
|
minLength: 2,
|
|
maxLength: 100,
|
|
})
|
|
@IsString({ message: 'El nombre debe ser texto' })
|
|
@IsNotEmpty({ message: 'El nombre es requerido' })
|
|
@MinLength(2, { message: 'El nombre debe tener al menos 2 caracteres' })
|
|
@MaxLength(100, { message: 'El nombre no puede exceder 100 caracteres' })
|
|
@Transform(({ value }) => value?.trim())
|
|
name: string;
|
|
```
|
|
|
|
### Número Entero
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Entero positivo
|
|
@ApiProperty({
|
|
description: 'Cantidad de items',
|
|
example: 10,
|
|
minimum: 1,
|
|
})
|
|
@IsInt({ message: 'La cantidad debe ser un número entero' })
|
|
@Min(1, { message: 'La cantidad mínima es 1' })
|
|
@Max(10000, { message: 'La cantidad máxima es 10,000' })
|
|
quantity: number;
|
|
|
|
// Entero con valor por defecto
|
|
@ApiPropertyOptional({
|
|
description: 'Página actual',
|
|
default: 1,
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(1)
|
|
@Transform(({ value }) => parseInt(value) || 1)
|
|
page?: number = 1;
|
|
```
|
|
|
|
### Número Decimal (Dinero)
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Precio/Dinero
|
|
@ApiProperty({
|
|
description: 'Precio del producto',
|
|
example: 99.99,
|
|
minimum: 0,
|
|
})
|
|
@IsNumber(
|
|
{ maxDecimalPlaces: 2 },
|
|
{ message: 'El precio debe tener máximo 2 decimales' }
|
|
)
|
|
@Min(0, { message: 'El precio no puede ser negativo' })
|
|
@Max(999999.99, { message: 'El precio máximo es 999,999.99' })
|
|
price: number;
|
|
```
|
|
|
|
### Boolean
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
@ApiProperty({
|
|
description: 'Estado activo del usuario',
|
|
example: true,
|
|
})
|
|
@IsBoolean({ message: 'El valor debe ser verdadero o falso' })
|
|
@IsNotEmpty()
|
|
isActive: boolean;
|
|
|
|
// Boolean opcional con default
|
|
@ApiPropertyOptional({
|
|
description: 'Enviar notificación',
|
|
default: false,
|
|
})
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Transform(({ value }) => value === 'true' || value === true)
|
|
sendNotification?: boolean = false;
|
|
```
|
|
|
|
### Enum
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
export enum UserStatus {
|
|
ACTIVE = 'active',
|
|
INACTIVE = 'inactive',
|
|
SUSPENDED = 'suspended',
|
|
}
|
|
|
|
@ApiProperty({
|
|
description: 'Estado del usuario',
|
|
enum: UserStatus,
|
|
example: UserStatus.ACTIVE,
|
|
})
|
|
@IsEnum(UserStatus, { message: 'El estado debe ser: active, inactive o suspended' })
|
|
@IsNotEmpty()
|
|
status: UserStatus;
|
|
```
|
|
|
|
### Fecha
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Fecha ISO
|
|
@ApiProperty({
|
|
description: 'Fecha de nacimiento',
|
|
example: '1990-05-15',
|
|
})
|
|
@IsISO8601({}, { message: 'La fecha debe estar en formato ISO 8601' })
|
|
@IsNotEmpty()
|
|
birthDate: string;
|
|
|
|
// Fecha como Date object
|
|
@ApiProperty({
|
|
description: 'Fecha de inicio',
|
|
example: '2024-01-15T10:30:00Z',
|
|
})
|
|
@IsDate({ message: 'Debe ser una fecha válida' })
|
|
@Type(() => Date)
|
|
@IsNotEmpty()
|
|
startDate: Date;
|
|
```
|
|
|
|
### URL
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR
|
|
@ApiPropertyOptional({
|
|
description: 'URL del avatar',
|
|
example: 'https://example.com/avatar.jpg',
|
|
})
|
|
@IsOptional()
|
|
@IsUrl({}, { message: 'La URL no es válida' })
|
|
@MaxLength(500)
|
|
avatarUrl?: string;
|
|
```
|
|
|
|
### Teléfono
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - México
|
|
@ApiPropertyOptional({
|
|
description: 'Número de teléfono',
|
|
example: '+521234567890',
|
|
})
|
|
@IsOptional()
|
|
@IsPhoneNumber('MX', { message: 'El número de teléfono no es válido' })
|
|
phone?: string;
|
|
|
|
// Alternativa con Regex
|
|
@IsOptional()
|
|
@Matches(/^\+?[1-9]\d{1,14}$/, { message: 'Formato de teléfono inválido' })
|
|
phone?: string;
|
|
```
|
|
|
|
### Array
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Array de strings
|
|
@ApiProperty({
|
|
description: 'Etiquetas del producto',
|
|
example: ['electrónica', 'ofertas'],
|
|
type: [String],
|
|
})
|
|
@IsArray({ message: 'Las etiquetas deben ser un arreglo' })
|
|
@IsString({ each: true, message: 'Cada etiqueta debe ser texto' })
|
|
@ArrayMinSize(1, { message: 'Debe haber al menos una etiqueta' })
|
|
@ArrayMaxSize(10, { message: 'Máximo 10 etiquetas permitidas' })
|
|
tags: string[];
|
|
|
|
// Array de UUIDs
|
|
@ApiProperty({
|
|
description: 'IDs de categorías',
|
|
type: [String],
|
|
})
|
|
@IsArray()
|
|
@IsUUID('4', { each: true, message: 'Cada ID debe ser un UUID válido' })
|
|
@ArrayMinSize(1)
|
|
categoryIds: string[];
|
|
```
|
|
|
|
### JSON/Object
|
|
|
|
```typescript
|
|
// PATRÓN ESTÁNDAR - Objeto flexible
|
|
@ApiPropertyOptional({
|
|
description: 'Metadatos adicionales',
|
|
example: { key: 'value' },
|
|
})
|
|
@IsOptional()
|
|
@IsObject({ message: 'Los metadatos deben ser un objeto' })
|
|
metadata?: Record<string, any>;
|
|
|
|
// Objeto con estructura definida (usar class anidada)
|
|
class AddressDto {
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
street: string;
|
|
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
city: string;
|
|
|
|
@IsString()
|
|
@Length(5, 5)
|
|
zipCode: string;
|
|
}
|
|
|
|
@ApiProperty({ type: AddressDto })
|
|
@ValidateNested()
|
|
@Type(() => AddressDto)
|
|
address: AddressDto;
|
|
```
|
|
|
|
---
|
|
|
|
## 3. DTOs ESTÁNDAR
|
|
|
|
### CreateDto Pattern
|
|
|
|
```typescript
|
|
/**
|
|
* DTO para crear usuario
|
|
*
|
|
* @example
|
|
* {
|
|
* "email": "user@example.com",
|
|
* "password": "SecurePass123!",
|
|
* "name": "Juan Pérez"
|
|
* }
|
|
*/
|
|
export class CreateUserDto {
|
|
@ApiProperty({ description: 'Email del usuario', example: 'user@example.com' })
|
|
@IsEmail()
|
|
@IsNotEmpty()
|
|
@MaxLength(255)
|
|
@Transform(({ value }) => value?.toLowerCase().trim())
|
|
email: string;
|
|
|
|
@ApiProperty({ description: 'Contraseña', example: 'SecurePass123!' })
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
@MinLength(8)
|
|
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
|
|
password: string;
|
|
|
|
@ApiProperty({ description: 'Nombre completo', example: 'Juan Pérez' })
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
@MinLength(2)
|
|
@MaxLength(100)
|
|
@Transform(({ value }) => value?.trim())
|
|
name: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Teléfono', example: '+521234567890' })
|
|
@IsOptional()
|
|
@IsPhoneNumber('MX')
|
|
phone?: string;
|
|
}
|
|
```
|
|
|
|
### UpdateDto Pattern
|
|
|
|
```typescript
|
|
import { PartialType, OmitType } from '@nestjs/swagger';
|
|
|
|
/**
|
|
* DTO para actualizar usuario
|
|
*
|
|
* Todos los campos son opcionales.
|
|
* Email y password NO se pueden actualizar aquí.
|
|
*/
|
|
export class UpdateUserDto extends PartialType(
|
|
OmitType(CreateUserDto, ['email', 'password'] as const)
|
|
) {}
|
|
```
|
|
|
|
### QueryDto Pattern (Filtros y Paginación)
|
|
|
|
```typescript
|
|
/**
|
|
* DTO para búsqueda y paginación de usuarios
|
|
*/
|
|
export class QueryUsersDto {
|
|
@ApiPropertyOptional({ description: 'Página', default: 1 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
page?: number = 1;
|
|
|
|
@ApiPropertyOptional({ description: 'Items por página', default: 20 })
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(100)
|
|
limit?: number = 20;
|
|
|
|
@ApiPropertyOptional({ description: 'Buscar por nombre o email' })
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(100)
|
|
search?: string;
|
|
|
|
@ApiPropertyOptional({ description: 'Filtrar por estado', enum: UserStatus })
|
|
@IsOptional()
|
|
@IsEnum(UserStatus)
|
|
status?: UserStatus;
|
|
|
|
@ApiPropertyOptional({ description: 'Ordenar por campo' })
|
|
@IsOptional()
|
|
@IsIn(['name', 'email', 'createdAt'])
|
|
sortBy?: string = 'createdAt';
|
|
|
|
@ApiPropertyOptional({ description: 'Dirección de orden', enum: ['asc', 'desc'] })
|
|
@IsOptional()
|
|
@IsIn(['asc', 'desc'])
|
|
sortOrder?: 'asc' | 'desc' = 'desc';
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. VALIDACIÓN EN SERVICE (Lógica de Negocio)
|
|
|
|
```typescript
|
|
@Injectable()
|
|
export class UserService {
|
|
async create(dto: CreateUserDto): Promise<UserEntity> {
|
|
// Validación de negocio (después de DTO)
|
|
|
|
// 1. Verificar unicidad
|
|
const existing = await this.repository.findOne({
|
|
where: { email: dto.email },
|
|
});
|
|
if (existing) {
|
|
throw new ConflictException('El email ya está registrado');
|
|
}
|
|
|
|
// 2. Validar reglas de negocio
|
|
if (await this.isEmailDomainBlocked(dto.email)) {
|
|
throw new BadRequestException('Dominio de email no permitido');
|
|
}
|
|
|
|
// 3. Validar límites
|
|
const userCount = await this.repository.count({
|
|
where: { tenantId: dto.tenantId },
|
|
});
|
|
if (userCount >= this.MAX_USERS_PER_TENANT) {
|
|
throw new ForbiddenException('Límite de usuarios alcanzado');
|
|
}
|
|
|
|
// Proceder con creación
|
|
const user = this.repository.create(dto);
|
|
return this.repository.save(user);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. VALIDACIÓN EN FRONTEND (React)
|
|
|
|
### Con React Hook Form + Zod
|
|
|
|
```typescript
|
|
import { z } from 'zod';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
// Schema de validación (espejo del DTO backend)
|
|
const createUserSchema = z.object({
|
|
email: z
|
|
.string()
|
|
.min(1, 'El email es requerido')
|
|
.email('Email inválido')
|
|
.max(255),
|
|
password: z
|
|
.string()
|
|
.min(8, 'Mínimo 8 caracteres')
|
|
.regex(
|
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
|
|
'Debe contener mayúscula, minúscula, número y carácter especial'
|
|
),
|
|
name: z
|
|
.string()
|
|
.min(2, 'Mínimo 2 caracteres')
|
|
.max(100, 'Máximo 100 caracteres'),
|
|
phone: z
|
|
.string()
|
|
.regex(/^\+?[1-9]\d{1,14}$/, 'Teléfono inválido')
|
|
.optional(),
|
|
});
|
|
|
|
type CreateUserForm = z.infer<typeof createUserSchema>;
|
|
|
|
// Uso en componente
|
|
const UserForm = () => {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<CreateUserForm>({
|
|
resolver: zodResolver(createUserSchema),
|
|
});
|
|
|
|
const onSubmit = async (data: CreateUserForm) => {
|
|
// data ya está validado
|
|
await userService.create(data);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<input {...register('email')} />
|
|
{errors.email && <span>{errors.email.message}</span>}
|
|
{/* ... */}
|
|
</form>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 6. MENSAJES DE ERROR ESTÁNDAR
|
|
|
|
```typescript
|
|
// Mensajes consistentes en español
|
|
const VALIDATION_MESSAGES = {
|
|
required: (field: string) => `${field} es requerido`,
|
|
minLength: (field: string, min: number) => `${field} debe tener al menos ${min} caracteres`,
|
|
maxLength: (field: string, max: number) => `${field} no puede exceder ${max} caracteres`,
|
|
email: 'El correo electrónico no es válido',
|
|
uuid: 'El ID no es válido',
|
|
enum: (values: string[]) => `El valor debe ser uno de: ${values.join(', ')}`,
|
|
min: (field: string, min: number) => `${field} debe ser mayor o igual a ${min}`,
|
|
max: (field: string, max: number) => `${field} debe ser menor o igual a ${max}`,
|
|
pattern: (field: string) => `${field} tiene un formato inválido`,
|
|
unique: (field: string) => `${field} ya existe`,
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## CHECKLIST DE VALIDACIÓN
|
|
|
|
```
|
|
DTO:
|
|
[ ] Cada campo tiene @ApiProperty o @ApiPropertyOptional
|
|
[ ] Campos requeridos tienen @IsNotEmpty
|
|
[ ] Campos opcionales tienen @IsOptional
|
|
[ ] Tipos validados (@IsString, @IsNumber, etc.)
|
|
[ ] Longitudes validadas (@MinLength, @MaxLength)
|
|
[ ] Formatos validados (@IsEmail, @IsUUID, etc.)
|
|
[ ] Transformaciones aplicadas (@Transform)
|
|
[ ] Mensajes de error en español
|
|
|
|
Service:
|
|
[ ] Validación de unicidad
|
|
[ ] Validación de existencia (referencias)
|
|
[ ] Validación de reglas de negocio
|
|
[ ] Validación de permisos
|
|
|
|
Frontend:
|
|
[ ] Schema Zod espeja DTO backend
|
|
[ ] Mensajes de error visibles
|
|
[ ] Validación en submit
|
|
[ ] Validación en blur (opcional)
|
|
```
|
|
|
|
---
|
|
|
|
**Versión:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patrón de Validación
|