Ángel David Roque Ayala
Andres Trujillo
gracias por los apuntes bro
Cuando necesitas filtrar productos dentro de un rango de precios específico, TypeORM ofrece operadores avanzados que simplifican esta tarea de forma elegante. Aprender a utilizar el operador Between y a construir condiciones dinámicas en tus consultas es fundamental para crear APIs robustas y flexibles.
¿Cómo funcionan las opciones avanzadas de búsqueda en TypeORM?
TypeORM proporciona dos niveles de búsqueda en sus repositorios. El nivel básico permite usar la cláusula where para filtrar por condiciones simples. Sin embargo, la parte avanzada incluye operadores SQL como Not, MoreThan, LessThan, MoreThanOrEqual y, el protagonista aquí, Between [01:10].
El operador Between permite definir un rango con un valor mínimo y un valor máximo. TypeORM traduce esto directamente a una sentencia SQL BETWEEN, lo que resulta ideal para filtrar precios, fechas o cualquier campo numérico.
¿Cómo configurar los DTOs para recibir un rango de precios?
El primer paso es agregar dos nuevos parámetros al DTO de filtros: minPrice y maxPrice [01:48]. Ambos se definen como positivos usando el decorador IsPositive() de class-validator, lo que impide enviar valores como cero o negativos.
¿Qué es ValidateIf y cómo garantiza que ambos valores lleguen juntos?
Una situación común es que si el usuario envía un precio mínimo, obligatoriamente debe enviar también el precio máximo para que el rango tenga sentido. Para esto se utiliza el decorador @ValidateIf() de class-validator [02:30]:
typescript @ValidateIf((item) => item.minPrice) @IsPositive() maxPrice: number;
Este decorador recibe una función que evalúa los atributos del DTO. En este caso, indica que maxPrice solo será validado (y por tanto requerido) cuando minPrice esté presente. Si no se envía minPrice, maxPrice tampoco será obligatorio.
¿Cómo construir un where dinámico en el servicio?
En lugar de tener un where fijo, la mejor práctica es construirlo de forma dinámica [03:30]. Esto permite agregar condiciones según los filtros que lleguen, ya sea por precio, marca, categoría u otro atributo:
typescript import { Between, FindOptionsWhere } from 'typeorm';
const where: FindOptionsWhere<Product> = {};
if (minPrice && maxPrice) { where.price = Between(minPrice, maxPrice); }
return this.productRepository.find({ where });
El tipo FindOptionsWhere (referido en la clase como FindConditions) permite tipar correctamente el objeto where con base en los atributos de la entidad Product [04:00]. Gracias a este tipado, al escribir where. el autocompletado mostrará únicamente las propiedades válidas del producto, lo que reduce errores.
Si ningún filtro está presente, el objeto where se envía vacío y la consulta retorna todos los registros sin restricciones.
¿Cómo probar el filtrado por rango en la práctica?
Al realizar pruebas con un cliente HTTP como Insomnia, se puede verificar el comportamiento del filtro [05:18]:
@ValidateIf() impide la petición y devuelve un error indicando que maxPrice es obligatorio.@IsPositive() rechaza la petición porque cero no es un número positivo [06:10].Estos resultados confirman que tanto las validaciones del DTO como el operador Between funcionan correctamente en conjunto.
Para depuración, se puede agregar un console.log que muestre los valores de minPrice y maxPrice tal como llegan al servicio, lo cual ayuda a identificar problemas rápidamente [07:40].
TypeORM ofrece muchos más operadores avanzados además de Between, como In, Like, IsNull o Raw. Te invito a revisar la documentación oficial y experimentar con diferentes combinaciones para construir filtros cada vez más potentes en tus APIs. ¿Qué otros operadores te parecen útiles para tus proyectos?
Ángel David Roque Ayala
Andres Trujillo
gracias por los apuntes bro
Jordan Salazar
A medida que va avanzando el curso quedo más maravillado con todo lo que puedo hacer con Nest JS ofrece.
Andres Trujillo
Me ha parecido una herramienta espectacular y te quita mucha complejidad algoritmica, espero que sigas aprendiendo y puedas conectar la con otras herramientas
Jairo Campos Ruiz
Andres Trujillo
Uf bro, interesante la implementación!
Filtrando precios con operadores
Vamos a agregar dos filtros más en nuestro DTO de filtros.
**src\products\dtos\products.dtos.ts**:export class FilterProductsDto { // ... @IsOptional() @IsPositive() minPrice: number; @IsOptional() @IsPositive() // esto es posible mientras halla un precio minimo @ValidateIf((item) => item.minPrice) maxPrice: number; }
Ahora vamos a nuestro servicio y configuremos estas opciones:
src\products\services\products.service.ts:
// importamos a Between y a FindOptionsWhere import { In, Repository, Between, FindOptionsWhere } from 'typeorm'; // importamos el DTO import { CreateProductDto } from '../dtos/product.dto'; @Injectable() export class ProductsService { // ... findAll(params?: FilterProductsDto) { // declaramos la instrucción where como un objeto vacío // será de tipo "FindOptionsWhere" en base a los atributos que tenga un producto const where: FindOptionsWhere<Product> = {}; const { limit, offset, maxPrice, minPrice } = params; // preguntamos si los parámetros existen if (minPrice && maxPrice) { // si existen // el precio estara entre el precio minimo al precio máximo where.price = Between(minPrice, maxPrice); } return this.productRepo.find({ relations: ['brand'], where, // añadimos el where a la consulta take: limit, skip: offset, }); } }
Y listo, de esta forma agregamos un filtro de precio con operadores que podemos utilizar al momento de realizar una consulta.
Nahuel Soma
En TypeORM 0.3.0, FindConditions es renombrado a FindOptionsWhere. Fuente: https://github.com/typeorm/typeorm/releases/tag/0.3.0
Juan Collado
Renato Renzo Luna Herrera
Consulta, a ver si pueden ayudarme, estuve buscando en los docs pero no encontré nada.
Que pasaría si digamos tengo una consulta muy compleja, que probablemente se pueda resolver con find-options, pero en este caso tendría que convertirlo, perdería tiempo, o incluso podría no resultar como necesito que funcione (podría).
Como hago para usar SQL directamente con TypeORM sin necesidad de usar las opciones de find?
Nicolas Mendoza
lo mejor es unsar una consulta sql. Es mas rapida si ya conoces sql.
Vanessa Paola Oliveros Padron
QueryBuilder
Andres Trujillo
Quisiera saber la opinión de ustedes sobre un tema en arquitectura:
DIEGO MAX NEYRA CORSINO
Un tip para alguien que este buscando porque no arroja error cuando el ValidateIf no se cumple, este lo único que impide es seguir con la validación mas no interrumpir el request.
Fuente:
ZANONI ALFREDO SALAS TOBÓN
Ricardo Tribaldos Hernández
Sería mejor ajustar la lógica para que puedas trabajar solo con uno o con el otro que es un caso de uso muy usado, además de validar que minPrice < maxPrice en caso de que te pasen ambos parámetros
ZANONI ALFREDO SALAS TOBÓN
Si, luego me toco quitarlo, por que generaba un error creo que con la paginación. :C
Gerardo Ferreyra
Para los que vienen siguiendo el curso con TypeORM 0.3+ FindConditions (where in FindOptions) fue renombrado a FindOptionsWhere.
Lo que hicimios
// src\products\dtos\products.dtos.ts @IsOptional() @IsPositive() minPrice: number; @ValidateIf((item) => item.minPrice) @IsPositive() maxPrice: number;
// src\products\services\products.service.ts // Cambiamos el servicio async findAll(params?: FilterProductDto) { if (params) { const where: FindConditions<Product> = {}; const { limit, offset } = params; const { maxPrice, minPrice } = params; if (minPrice && maxPrice) { where.price = Between(minPrice, maxPrice); } return this.productRepo.find({ relations: ['brand'], where, take: limit, skip: offset, }); } return this.productRepo.find({ relations: ['brand'], }); }
Aquí dejo un ejemplo con más condiciones, con ordenamiento y alguna que otra cosa más:
Para dividir un poco el Dto me cree una clase de la que heredarán los otros filter con limit y offset ya que estos son muy comunes en cualquier filtro
import { IsOptional, IsPositive, Min } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class BaseFilterDto { @ApiProperty() @IsOptional() @IsPositive() readonly limit: number; @ApiProperty() @IsOptional() @Min(0) readonly offset: number; }
Al Dto de filtrar productos le añadí varias opciones más como que puedas pasar solo un minimo o maximo precio, ordenar por un parámetro en concreto, orden descendente o ascendente y buscar por id de marca.
export class FilterProductsDto extends BaseFilterDto { @ApiProperty() @IsOptional() @Min(0) readonly minPrice: number; @ApiProperty() @IsOptional() @IsPositive() readonly maxPrice: number; @ApiProperty({ enum: ['price', 'id'] }) @ValidateIf((item) => item.order) @IsEnum(['price', 'id']) readonly orderBy: string; @ApiProperty({ enum: ['ASC', 'DESC'] }) @ValidateIf((item) => item.orderBy) @IsEnum(['ASC', 'DESC']) readonly order: string; @ApiProperty() @IsOptional() @IsNumber() readonly brandId: number; }
Para terminar el service me queda de la siguiente manera:
async findAll(params?: FilterProductsDto) { const { limit, offset, maxPrice, minPrice, orderBy, order, brandId } = params; const findOptions: FindManyOptions<Product> = {}; const where: FindConditions<Product> = {}; if (minPrice && maxPrice) { where.price = Between(minPrice, maxPrice); } else if (minPrice) { where.price = MoreThanOrEqual(minPrice); } else if (maxPrice) { where.price = LessThanOrEqual(maxPrice); } if (brandId) where.brand = { id: brandId }; if (orderBy && order) findOptions.order = { [orderBy]: order }; findOptions.where = where; findOptions.skip = offset; findOptions.take = limit; findOptions.relations = ['brand', 'categories']; return await this.productRepository.find(findOptions); }
Validar maxPrice > minPrice con class-validator
Se puede crear un custom decorator para validar que el valor de maxPrice sea mayor al de minPrice con class-validator. Este también va a poder ser reutilizado en otras situaciones en las que pueda ser necesario. https://github.com/typestack/class-validator#custom-validation-decorators
// src/common/IsGreaterThan.validator.ts import { registerDecorator, ValidationOptions, ValidationArguments, } from 'class-validator'; export function IsGreaterThan( property: string, // Propiedad con la que se va a comparar (minPrice) defaultValue: number, // Valor por defecto (en caso de que no haya un minPrice, por ejemplo) validationOptions?: ValidationOptions, ) { return function (object: unknown, propertyName: string) { registerDecorator({ name: 'isGreaterThan', target: object.constructor, propertyName, constraints: [property], options: validationOptions, validator: { validate(value: any, args: ValidationArguments) { const [relatedPropertyName] = args.constraints; const relatedValue = (args.object as any)[relatedPropertyName] || defaultValue; return ( typeof value === 'number' && typeof relatedValue === 'number' && value > relatedValue ); }, }, }); }; }
// src/products/dtos/products.dtos.ts import { IsGreaterThan } from 'src/common/IsGreaterThan.validator'; import { IsOptional, isPositive } from 'class-validator'; export class FilterProductDto { @IsOptional() @IsPositive() minPrice: number; // En caso de que no haya un minPrice, tomara el valor 0 @IsOptional() @IsGreaterThan('minPrice', 0, { message: 'Max price have to be greater than min price.', }) maxPrice: number; }
Es necesario hacer las validaciones a ambos campos o el otro pasa como vacío
@IsPositive() @ValidateIf((item) => item.minPrice) maxPrice: number @IsPositive() @ValidateIf((item) => item.maxPrice) minPrice: number