@ciphercross/nestjs-filters
v1.0.0
Published
NestJS filters library with DTOs, utilities, and enums for pagination, sorting, searching, and filtering. Designed to work seamlessly with Prisma ORM and @ciphercross/nestjs-location for geographic filtering.
Readme
@ciphercross/nestjs-filters
NestJS filters library with DTOs, utilities, and enums for pagination, sorting, searching, and filtering. Designed to work seamlessly with Prisma ORM.
Installation
npm install @ciphercross/nestjs-filtersPeer Dependencies
This library requires the following peer dependencies:
@nestjs/common(^10.0.0 || ^11.0.0)@nestjs/swagger(^7.0.0 || ^11.0.0)class-validator(^0.14.0)class-transformer(^0.5.0)
Integration with @ciphercross/nestjs-location
This library works seamlessly with @ciphercross/nestjs-location for geographic filtering. The GeoFiltersDto provides DTO validation, while @ciphercross/nestjs-location handles the actual distance calculations and filtering.
Recommended: Install both packages for complete geographic filtering functionality:
npm install @ciphercross/nestjs-filters @ciphercross/nestjs-locationFeatures
- Pagination: Built-in pagination DTOs and utilities
- Sorting: Sort field validation and Prisma orderBy builders
- Search: Multi-field search filters with nested field support
- Date Filtering: Date range filters and time-based filters (today, last 7 days, etc.)
- Numeric Ranges: Min/max filters for numeric fields
- Geographic Filters: Latitude/longitude and distance filtering
- Type-Safe: Full TypeScript support with proper types
Quick Start
1. Create a Custom Filter DTO
Extend BaseListFiltersDto to create your own filter DTO:
import { BaseListFiltersDto, GeoFiltersDto } from '@ciphercross/nestjs-filters';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class ServicesFiltersDto extends BaseListFiltersDto implements GeoFiltersDto {
// Inherits: page, limit, sortBy, sortOrder, search
// Geo filters
lat?: number;
lng?: number;
maxDistance?: number;
// Custom filters
@ApiPropertyOptional()
@IsOptional()
@IsString()
category?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
businessCategory?: string;
}2. Use in Controller
import { Controller, Get, Query } from '@nestjs/common';
import { ServicesFiltersDto } from './dto/services-filters.dto';
@Controller('services')
export class ServicesController {
@Get()
async findAll(@Query() filters: ServicesFiltersDto) {
// filters.page, filters.limit, filters.search, etc.
}
}3. Build Prisma Queries
import {
buildPagination,
buildPrismaOrderBy,
buildSearchFilter,
buildRangeFilter,
} from '@ciphercross/nestjs-filters';
async getServices(filters: ServicesFiltersDto) {
// Pagination
const { skip, take } = buildPagination(filters.page, filters.limit);
// Sorting
const orderBy = buildPrismaOrderBy(
filters.sortBy,
filters.sortOrder,
['createdAt', 'price', 'name'], // allowed fields
'createdAt', // default field
'desc' // default order
);
// Search
const searchFilter = buildSearchFilter(filters.search, ['title', 'description']);
// Numeric range
const priceFilter = buildRangeFilter('price', filters.minPrice, filters.maxPrice);
// Build where clause
const where = {
...searchFilter,
...priceFilter,
// ... other filters
};
return this.prisma.service.findMany({
where,
skip,
take,
orderBy,
});
}API Reference
DTOs
PaginationFilterDto
Base pagination DTO with page and limit fields.
class PaginationFilterDto {
page?: number = 1;
limit?: number = 20;
}SortFilterDto
Sorting DTO with sortBy and sortOrder fields.
class SortFilterDto {
sortBy?: string;
sortOrder?: SortOrder = SortOrder.DESC;
}SearchFilterDto
Search DTO with search field.
class SearchFilterDto {
search?: string;
}GeoFiltersDto
Geographic filters with latitude, longitude, and max distance. Designed to work with @ciphercross/nestjs-location for distance calculations and filtering.
class GeoFiltersDto {
lat?: number; // -90 to 90
lng?: number; // -180 to 180
maxDistance?: number; // 0.1 to 1000 km
}Note: This DTO only provides validation. For actual distance filtering, use DistanceFilterService from @ciphercross/nestjs-location.
NumericRangeFilterDto
Base numeric range filter with min and max fields.
class NumericRangeFilterDto {
min?: number;
max?: number;
}DateRangeFilterDto
Date range filter with startDate, endDate, and date fields.
class DateRangeFilterDto {
startDate?: string; // ISO date string
endDate?: string; // ISO date string
date?: string; // Single date (ISO date string)
}BaseListFiltersDto
Combines pagination, sorting, and search. Extend this for your custom filters.
class BaseListFiltersDto extends PaginationFilterDto
implements SortFilterDto, SearchFilterDto {
sortBy?: string;
sortOrder?: 'asc' | 'desc';
search?: string;
}Enums
SortOrder
enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}TimeFilter
enum TimeFilter {
ALL = 'all',
TODAY = 'today',
LAST_7_DAYS = 'last_7_days',
LAST_30_DAYS = 'last_30_days',
EARLIER = 'earlier',
}Utilities
buildPagination(page?, limit?)
Builds Prisma pagination parameters.
const { skip, take } = buildPagination(1, 20);
// Returns: { skip: 0, take: 20 }buildPrismaOrderBy(sortBy, sortOrder, allowedFields, defaultField, defaultOrder)
Builds Prisma orderBy object with validation.
const orderBy = buildPrismaOrderBy(
'price',
'asc',
['createdAt', 'price', 'name'],
'createdAt',
'desc'
);
// Returns: { price: 'asc' }buildSearchFilter(searchTerm, fields[])
Builds Prisma search filter for multiple fields.
const searchFilter = buildSearchFilter('spa', ['name', 'description']);
// Returns: {
// OR: [
// { name: { contains: 'spa', mode: 'insensitive' } },
// { description: { contains: 'spa', mode: 'insensitive' } }
// ]
// }buildNestedSearchFilter(searchTerm, nestedFields[])
Builds Prisma search filter for nested fields.
const nestedFilter = buildNestedSearchFilter('spa', [
{ path: ['business'], field: 'name' },
{ path: ['category'], field: 'title' }
]);buildRangeFilter(field, min?, max?)
Builds Prisma numeric range filter.
const priceFilter = buildRangeFilter('price', 10, 100);
// Returns: { price: { gte: 10, lte: 100 } }buildPrismaDateRangeFilter(field, startDate?, endDate?, singleDate?)
Builds Prisma date range filter.
const dateFilter = buildPrismaDateRangeFilter(
'createdAt',
'2024-01-01',
'2024-12-31'
);
// Returns: { createdAt: { gte: Date, lte: Date } }buildTimeFilterWhere(field, timeFilter?)
Builds Prisma where clause from TimeFilter enum.
const timeFilter = buildTimeFilterWhere('createdAt', TimeFilter.LAST_7_DAYS);
// Returns: { createdAt: { gte: Date } }validateSortField(sortBy, allowedFields, defaultField)
Validates sort field against allowed fields.
const field = validateSortField('price', ['createdAt', 'price'], 'createdAt');validateSortOrder(sortOrder, defaultOrder)
Validates sort order.
const order = validateSortOrder('asc', 'desc');
// Returns: 'asc'Examples
Example 1: Services with Multiple Filters (with Location)
import {
BaseListFiltersDto,
GeoFiltersDto,
buildPagination,
buildPrismaOrderBy,
buildSearchFilter,
buildRangeFilter,
} from '@ciphercross/nestjs-filters';
import { DistanceFilterService } from '@ciphercross/nestjs-location';
export class ServicesFiltersDto extends BaseListFiltersDto implements GeoFiltersDto {
lat?: number;
lng?: number;
maxDistance?: number;
minPrice?: number;
maxPrice?: number;
category?: string;
}
// In service
@Injectable()
export class ServicesService {
constructor(
private readonly prisma: PrismaService,
private readonly distanceFilterService: DistanceFilterService,
) {}
async getServices(filters: ServicesFiltersDto) {
// Build Prisma query filters
const where = {
...buildSearchFilter(filters.search, ['title', 'description']),
...buildRangeFilter('price', filters.minPrice, filters.maxPrice),
...(filters.category && { category: { slug: filters.category } }),
};
const orderBy = buildPrismaOrderBy(
filters.sortBy,
filters.sortOrder,
['createdAt', 'price'],
'createdAt'
);
// Fetch data from database
const services = await this.prisma.service.findMany({
where,
include: { location: true }, // Include location data
});
// Apply geographic filtering if coordinates provided
let filteredServices = services;
if (filters.lat && filters.lng && filters.maxDistance) {
filteredServices = this.distanceFilterService.applyDistanceFilter(
services,
filters.lat,
filters.lng,
filters.maxDistance,
);
}
// Apply pagination after filtering
const { skip, take } = buildPagination(filters.page, filters.limit);
const paginatedServices = filteredServices.slice(skip, skip + take);
return {
data: paginatedServices,
total: filteredServices.length,
page: filters.page || 1,
limit: filters.limit || 20,
};
}
}Example 2: Visits with Time Filter
import {
PaginationFilterDto,
TimeFilter,
buildTimeFilterWhere,
buildPagination,
} from '@ciphercross/nestjs-filters';
import { IsOptional, IsEnum, IsString } from 'class-validator';
export class VisitFiltersDto extends PaginationFilterDto {
@IsOptional()
@IsEnum(TimeFilter)
timeFilter?: TimeFilter = TimeFilter.ALL;
@IsOptional()
@IsString()
businessId?: string;
}
// In service
async getVisits(filters: VisitFiltersDto) {
const { skip, take } = buildPagination(filters.page, filters.limit);
const where = {
...buildTimeFilterWhere('createdAt', filters.timeFilter),
...(filters.businessId && { businessId: filters.businessId }),
};
return this.prisma.visit.findMany({ where, skip, take });
}Example 3: Reviews with Rating Range
import {
BaseListFiltersDto,
NumericRangeFilterDto,
buildRangeFilter,
} from '@ciphercross/nestjs-filters';
export class ReviewFiltersDto extends BaseListFiltersDto implements NumericRangeFilterDto {
min?: number; // minRating
max?: number; // maxRating
businessId?: string;
}
// In service
async getReviews(filters: ReviewFiltersDto) {
const { skip, take } = buildPagination(filters.page, filters.limit);
const where = {
...buildRangeFilter('rating', filters.min, filters.max),
...(filters.businessId && { businessId: filters.businessId }),
};
return this.prisma.review.findMany({ where, skip, take });
}Integration Example: Full Location-Based Search
Complete example using both @ciphercross/nestjs-filters and @ciphercross/nestjs-location:
import { Module } from '@nestjs/common';
import { LocationModule } from '@ciphercross/nestjs-location';
import {
BaseListFiltersDto,
GeoFiltersDto,
buildPagination,
buildSearchFilter,
} from '@ciphercross/nestjs-filters';
import { LocationService } from '@ciphercross/nestjs-location';
@Module({
imports: [LocationModule.forRoot()],
})
export class AppModule {}
// DTO
export class BusinessFiltersDto extends BaseListFiltersDto implements GeoFiltersDto {
lat?: number;
lng?: number;
maxDistance?: number;
category?: string;
}
// Service
@Injectable()
export class BusinessService {
constructor(
private readonly prisma: PrismaService,
private readonly locationService: LocationService,
) {}
async findNearbyBusinesses(filters: BusinessFiltersDto) {
// 1. Build base Prisma filters
const where = {
...buildSearchFilter(filters.search, ['name', 'description']),
...(filters.category && { categories: { some: { slug: filters.category } } }),
};
// 2. Fetch businesses with location data
const businesses = await this.prisma.business.findMany({
where,
include: { location: true },
});
// 3. Apply location filtering if coordinates provided
if (filters.lat && filters.lng && filters.maxDistance) {
const result = this.locationService.findNearest(
businesses,
filters.lat,
filters.lng,
filters.maxDistance,
{
page: filters.page || 1,
limit: filters.limit || 20,
sortBy: SortBy.DISTANCE,
},
);
return result;
}
// 4. Apply pagination for non-location queries
const { skip, take } = buildPagination(filters.page, filters.limit);
return {
items: businesses.slice(skip, skip + take),
total: businesses.length,
page: filters.page || 1,
limit: filters.limit || 20,
};
}
}License
UNLICENSED
