Filter by relation in Drizzle ORM
Drizzle filter by relation in NestJS: paths at any depth and some, none, every on collections, compiled to EXISTS subqueries that never multiply rows.
To filter by relation with Drizzle in this package, use the relation name as a filter key: { state: { country: { code: "IN" } } } follows a path through relations to any depth, and { states: { none: { name: "Kerala" } } } quantifies a collection with some, none or every. Only the top-level belongs-to is joined. Everything else compiles to an IN, EXISTS or NOT EXISTS subquery, so parent rows are never multiplied and total stays right.
Declare the relations first
A relation filter only works on relations declared in the entity's relations config, and a
collection needs type: "hasMany" or type: "manyToMany". Without a type the relation is a
belongs-to. Relations covers the config; Filtering with Drizzle in
NestJS covers the column operators used inside a relation filter.
// Cities in India: city -> state -> country
await this.cities.findAll({ state: { country: { code: "IN" } } });
// Countries with no state called Kerala
await this.countries.findAll({ states: { none: { name: "Kerala" } } });How do I filter through more than one relation?
Nest the relation names, as declared in each relation's own relations config. A belongs-to chain from cities to countries looks like this:
relations: {
state: {
table: states,
localKey: "state_id",
relations: {
country: { table: countries, localKey: "country_id" },
},
},
}await this.cities.findAll({ state: { country: { code: "IN" } } });
await this.cities.count({ state: { name: "Kerala", country: { code: "IN" } } });The top-level belongs-to (state) is joined with a LEFT JOIN. Each deeper level is a correlated EXISTS on its own table alias:
select ... from cities
left join states on cities.state_id = states.id
where exists (
select 1 from countries crud_sq1
where crud_sq1.id = states.country_id and lower(crud_sq1.code) = lower($1)
)Because each subquery gets its own alias, a relation back to a table already in the query (a self relation) still resolves. With soft delete on, a soft-deleted related row never matches, at any level.
How do I use some, none and every?
On a has-many or many-to-many relation, the value is either a filter (meaning some) or an object whose keys are all quantifiers:
| Filter | Keeps parents that | SQL |
|---|---|---|
{ states: { name: "Kerala" } } | have at least one matching row | id IN (SELECT ...) |
{ states: { some: { name: "Kerala" } } } | the same | id IN (SELECT ...) |
{ states: { some: {} } } | have any row at all | id IN (SELECT ...) |
{ states: { none: { name: "Kerala" } } } | have no matching row | NOT EXISTS (...) |
{ states: { none: {} } } | have no rows at all | NOT EXISTS (...) |
{ states: { every: { name: { startsWith: "Ba" } } } } | have no row that fails the filter | NOT EXISTS (... AND NOT coalesce(filter, false)) |
Rules worth knowing:
- Several quantifiers in one object are ANDed:
{ states: { some: { name: "Kerala" }, none: { code: "XX" } } }. - An empty plain filter,
{ states: {} }, adds nothing. Write{ states: { some: {} } }to mean "has at least one". - The object counts as quantifiers only when every key is
some,noneoreveryand the related table has no column by that name. Mixing a quantifier with a column ({ some: {...}, name: "x" }) is read as a plain filter, sosomeis an unknown column and the call is a 400. - A quantifier value must be an object of column filters, otherwise 400.
What does every mean for a parent with no rows?
It matches. every means "no related row fails the filter", which is true when there are none, as in Prisma. Add some: {} when you need at least one:
await this.countries.findAll({
states: { some: {}, every: { name: { startsWith: "Ba" } } },
});A related row whose filter evaluates to NULL (a NULL column, say) counts as failing, which is what the coalesce(..., false) is for. Soft-deleted related rows are ignored by all three quantifiers. An every with an empty filter adds nothing.
Do relation filters work in groups and counts?
Yes. A relation filter, at any depth and with any quantifier, can sit inside and, or and not, and findAll, findByCursor, count and countBy all build their WHERE with the same compiler:
await this.countries.count({
or: [{ states: { every: { name: { startsWith: "Ba" } } } }, { code: "IN" }],
});
await this.cities.count({ not: { state: { country: { code: "IN" } } } });
await this.cities.count({ state: { cities: { some: { name: "Mysuru" } } } }); // a collection under a belongs-to
await this.countries.count({ states: { cities: { some: { name: "Mysuru" } } } }); // a collection under a collectionHow do I send relation filters in a query string?
Use brackets or dots, the same as for one level. parseQuery builds the filter object for you:
GET /cities?state[country][code]=IN
GET /cities?state.country.code=IN
GET /countries?states[none][name]=Kerala
GET /countries?states[every][name][startsWith]=Ba
GET /countries?states[some]=states[some]= with no column means "has at least one".
How do allowlists treat a relation path?
filterableColumns names the full dotted path to the column. "state.country.code" allows the city filter above and nothing else on country. Quantifiers do not appear in the path, so "states.name" allows states[none][name] as well as states[name]. See allowlists.
Common questions
No. A has-many or many-to-many filter compiles to IN (SELECT ...) or NOT EXISTS (...), never a join, so each parent appears once and total and pagination stay correct.
Only a belongs-to at the top level of the filter, with a LEFT JOIN. A belongs-to deeper in a path is an EXISTS subquery, and a collection is always a subquery.
Yes, as in Prisma. Combine it with some: {} when the parent must have at least one related row.
No. With soft delete on for the related table, a soft-deleted row never matches a relation filter, and some, none and every all ignore it.
Next steps
Filtering with Drizzle in NestJS
Every column operator, and or, and, not groups.
Relations
Declare belongs-to, has-many and many-to-many relations.
Aggregates
Count related rows per parent with counts and countBy.
Filtering with Drizzle in NestJS
Drizzle filtering in NestJS with findAll and count. Pass exact values, arrays, operators such as gte, between and contains, and or, and, not groups.
NestJS query string filters for Drizzle
Parse NestJS query string filters for Drizzle with parseQuery. It turns ?age[gte]=18&status=active into typed filters, pagination and options.