Customizing the Query
How to use the `customize` parameter of `execute()` to add extra conditions, and how to implement text search.
The execute() method accepts a fourth optional parameter: a callback that receives the SelectQueryBuilder with filters, sorts, includes, and fields already applied, allowing you to add extra clauses before execution.
Rule of thumb: in the library's public parameters (filters, sorts,
fields, includes, search), use entity property names. In customize,
when you write manual SQL as a string, you can use the database's physical
column names.
execute(repo, query, rules, customize?: (qb: SelectQueryBuilder<T>) => void)When to use it
| Use case | Approach |
|---|---|
| Internal conditions not exposed to the client (soft delete, tenant, active status) | customize |
| Text search outside the library default | customize |
Filtering by the authenticated user (userId = :id) | customize |
| Manual JOINs the library does not cover | customize |
| Filters the client should control | @ApiDynamicQuery with filters |
| Relation loading | includes in RulesConfig |
Basic example
Force a condition that should never be exposed as a public filter:
@Get()
@ApiDynamicQuery({
filters: ['name', 'email'],
sorts: ['name', 'createdAt'],
})
async findAll(
@Query() query: DynamicQueryDto,
@QueryRules() rules: RulesConfig,
) {
return this.queryBuilderService.execute(
this.usersRepo,
query,
rules,
(qb) => {
qb.andWhere('root.status = :status', { status: 'active' });
},
);
}Text search (search)
The library now has native search. Declare the searchable fields in RulesConfig.search and send ?search=term:
@Get()
@ApiDynamicQuery({
filters: ['name', 'createdAt'],
sorts: ['name', 'createdAt'],
search: ['name', 'email', 'company.name'],
})
async findAll(
@Query() query: DynamicQueryDto,
@QueryRules() rules: RulesConfig,
) {
return this.queryBuilderService.execute(this.usersRepo, query, rules);
}Usage: GET /users?search=john
The fields in search are combined with OR, using case-insensitive text search. This also works for nested relations such as company.name and items.company.cnpj - the library reuses includes joins when they exist and creates simple joins when needed.
search is designed for a quick search box. The client sends only the term;
the backend decides which fields can be searched.
When to still use customize for search
Use customize when search needs to deviate from the library's default behavior, for example:
- different weights per field
ANDbetween term groups- database-specific full-text search
- conditional rules by tenant, role, or authentication context
When to use search instead of filter
Use filter when the client needs to control which field to filter and which operator to use. Use search when the frontend only needs a quick search box and the backend decides which fields are searchable:
filter[name][like]=john | search=john | |
|---|---|---|
| Field control | client | backend |
| Library whitelist | yes | yes, via RulesConfig.search |
| Multiple fields simultaneously | not directly | yes (OR) |
| Performance | field index | may require a composite index |
Using buildQuery for full control
Prefer buildQuery() when you need full control over execution (for example, getManyAndCount, complex JOINs):
const qb = this.queryBuilderService.buildQuery(repo, query, rules);
qb.innerJoin('root.company', 'company').andWhere('company.id = :companyId', {
companyId,
});
const [data, total] = await qb.getManyAndCount();buildQuery() returns the SelectQueryBuilder without executing the query - all filters, sorts, includes, and fields are already applied.