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 isNotEmptyexport class Request { @isString() @isNotEmpty() name?: string}
// El orden de ejecución real es:// 1. isNotEmpty (se registra primero, se ejecuta primero)// 2. isStringFormato 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ónexport class AddressDto { @isString() @isNotEmpty() street?: string
@isString() @isNotEmpty() city?: string
@isString() @isNotEmpty() country?: string
@isString() @isOptional() zipCode?: string}
// Modelo anidado: teléfonoexport class PhoneDto { @isString() @isIn(['mobile', 'home', 'work']) type?: string
@isString() @isNotEmpty() number?: string}
// Request de creación con modelos anidados y arraysexport 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.