Skip to content

Validación y Transformación de Datos

Wabot incluye un sistema de validación basado en decoradores que te permite definir reglas directamente en las propiedades de tus clases. El framework valida automáticamente los datos en endpoints REST, funciones de mindset y eventos socket.

Conceptos clave

  • ValidationMetadataStore: almacén singleton que registra todos los validadores asociados a cada clase.
  • Decoradores de validación: se aplican como decoradores de propiedad y definen las reglas que deben cumplir los datos.
  • validateAndTransform(data, Class): función que ejecuta la validación y devuelve el resultado tipado o los errores.
  • Mapper: servicio inyectable que combina validación + transformación + copia profunda.

Validadores de tipo

Verifican que el valor sea del tipo esperado.

import { isString, isNumber, isBoolean, isDate } from '@wabot-dev/framework'
export class ProductRequest {
@isString()
name?: string
@isNumber()
price?: number
@isBoolean()
active?: boolean
@isDate()
releaseDate?: Date // Transforma strings y timestamps a Date
}

@isDate() es especial: además de validar, transforma automáticamente strings ISO y timestamps numéricos en objetos Date.

Validadores de presencia

Controlan si un valor es requerido o puede omitirse.

import { isString, isPresent, isNotEmpty, isOptional } from '@wabot-dev/framework'
export class ContactRequest {
@isString()
@isPresent() // No puede ser null ni undefined
@isNotEmpty() // No puede ser string vacío ni array vacío
name?: string
@isString()
@isOptional() // Si es null/undefined, se saltan TODOS los demás validadores
nickname?: string
}

Validadores de rango

Restringen los valores numéricos o limitan a un conjunto de opciones.

import { isNumber, isString, min, max, isIn } from '@wabot-dev/framework'
export class OfferRequest {
@isNumber()
@min(1)
@max(1000)
quantity?: number
@isNumber()
@min(0)
@max(100)
discount?: number
@isString()
@isIn(['draft', 'sent', 'accepted', 'rejected'])
status?: string
}

Colecciones

@isArray()

Valida que el valor sea un array. Los validadores hermanos (como @isString()) se aplican automáticamente a cada elemento del array.

import { isArray, isString, isNotEmpty } from '@wabot-dev/framework'
export class TagsRequest {
@isArray({ minLength: 1, maxLength: 10 })
@isString() // Cada elemento debe ser string
@isNotEmpty() // Cada elemento no puede estar vacío
tags?: string[]
}

@isModel()

Valida recursivamente un objeto anidado usando otra clase como modelo.

import { isString, isNumber, isModel, isNotEmpty } from '@wabot-dev/framework'
export class Address {
@isString()
@isNotEmpty()
street?: string
@isString()
@isNotEmpty()
city?: string
}
export class UserRequest {
@isString()
@isNotEmpty()
name?: string
@isModel(Address)
address?: Address
}

@isRecord()

Valida objetos clave-valor (mapas).

import { isRecord } from '@wabot-dev/framework'
export class MetadataRequest {
@isRecord('string', 'string')
metadata?: Record<string, string>
@isRecord('string', 'number')
scores?: Record<string, number>
}

Orden de decoradores

Los decoradores se ejecutan de abajo hacia arriba. El orden correcto coloca los validadores más generales arriba y los más específicos abajo:

// Correcto: isString valida primero, luego isNotEmpty
export class Request {
@isString()
@isNotEmpty()
name?: string
}
// El orden de ejecución real es:
// 1. isNotEmpty (se registra primero, se ejecuta primero)
// 2. isString

Formato de errores

Cuando la validación falla, el error tiene esta estructura:

{
description: 'Validation failed',
properties: {
name: ['isNotEmpty: value must not be empty'],
price: ['isNumber: value must be a number', 'min: value must be >= 0'],
address: {
description: 'Validation failed',
properties: {
street: ['isNotEmpty: value must not be empty']
}
}
}
}

Mapper

El Mapper es un servicio inyectable que combina validación, transformación y copia profunda en una sola operación. Es la forma recomendada de transformar datos raw a DTOs tipados.

import { injectable, Mapper } from '@wabot-dev/framework'
@injectable()
export class UserService {
constructor(private mapper: Mapper) {}
createUser(rawData: unknown) {
// Valida, transforma y devuelve una instancia tipada
// Lanza CustomError(500) si la validación falla
const user = this.mapper.map(rawData, CreateUserRequest)
return user
}
}

El Mapper maneja automáticamente:

  • Instancias de Storable: preserva la referencia original.
  • Objetos Date: crea copias correctas.
  • Propiedades getter-only: las omite durante la copia.
  • Validación: ejecuta todos los decoradores registrados.

Uso en el framework

Endpoints REST

Los controladores REST validan automáticamente el body, query params y path params. El framework los combina en un solo objeto y lo valida contra la clase del primer parámetro del método:

import { restController, onPost, isString, isNotEmpty, isNumber, min } from '@wabot-dev/framework'
export class CreateProductRequest {
@isString()
@isNotEmpty()
name?: string
@isNumber()
@min(0)
price?: number
}
@restController('/products')
export class ProductController {
@onPost()
async create(req: CreateProductRequest) {
// req ya está validado y tipado
return { created: req.name, price: req.price }
}
}

Funciones de mindset

El decorador @param en funciones de mindset también usa el sistema de validación para asegurar que los datos extraídos por el LLM sean válidos.

Eventos socket

Los handlers de eventos socket validan el payload de la misma forma que los endpoints REST.

Ejemplo completo

Un sistema de validación para gestión de usuarios con modelos anidados y arrays:

import {
isString, isNumber, isBoolean, isDate,
isNotEmpty, isOptional, isPresent,
min, max, isIn,
isArray, isModel, isRecord,
restController, onPost, onPut,
} from '@wabot-dev/framework'
// Modelo anidado: dirección
export class AddressDto {
@isString()
@isNotEmpty()
street?: string
@isString()
@isNotEmpty()
city?: string
@isString()
@isNotEmpty()
country?: string
@isString()
@isOptional()
zipCode?: string
}
// Modelo anidado: teléfono
export class PhoneDto {
@isString()
@isIn(['mobile', 'home', 'work'])
type?: string
@isString()
@isNotEmpty()
number?: string
}
// Request de creación con modelos anidados y arrays
export class CreateUserRequest {
@isString()
@isNotEmpty()
name?: string
@isString()
@isNotEmpty()
email?: string
@isNumber()
@min(18)
@max(120)
age?: number
@isString()
@isIn(['admin', 'editor', 'viewer'])
role?: string
@isModel(AddressDto)
address?: AddressDto
@isArray({ minLength: 1, maxLength: 5 })
@isModel(PhoneDto)
phones?: PhoneDto[]
@isArray()
@isString()
@isOptional()
tags?: string[]
@isRecord('string', 'string')
@isOptional()
metadata?: Record<string, string>
}
// Request de actualización (todo opcional)
export class UpdateUserRequest {
@isString()
@isNotEmpty()
id?: string
@isString()
@isOptional()
name?: string
@isString()
@isOptional()
email?: string
@isNumber()
@min(18)
@max(120)
@isOptional()
age?: number
}
// Controlador que usa las clases validadas
@restController('/users')
export class UserController {
@onPost()
async create(req: CreateUserRequest) {
// req está validado: name no vacío, age 18-120,
// address validado recursivamente, phones tiene 1-5 items, etc.
return { message: 'Usuario creado', name: req.name }
}
@onPut('/:id')
async update(req: UpdateUserRequest) {
// Solo los campos presentes se validan gracias a @isOptional
return { message: 'Usuario actualizado', id: req.id }
}
}

Siguiente paso

Aprende a persistir datos con entidades y repositorios: Guarda información.