Filtering
It is possible to define filters for Django types, which will
be converted into .filter(...)
queries for the ORM:
import strawberry_djangofrom strawberry import auto
@strawberry_django.filter(models.Fruit)class FruitFilter: id: auto name: auto
@strawberry_django.type(models.Fruit, filters=FruitFilter)class Fruit: ...
Tip
In most cases filter fields should have Optional
annotations and default value strawberry.UNSET
like so:
foo: Optional[SomeType] = strawberry.UNSET
Above auto
annotation is wrapped in Optional
automatically.
UNSET
is automatically used for fields without field
or with strawberry_django.filter_field
.
The code above would generate following schema:
input FruitFilter { id: ID name: String AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean}
Tip
If you are using the relay integration and working with types inheriting
from relay.Node
and GlobalID
for identifying objects, you might want to set
MAP_AUTO_ID_AS_GLOBAL_ID=True
in your strawberry django settings
to make sure auto
fields gets mapped to GlobalID
on types and filters.
AND, OR, NOT, DISTINCT …
To every filter AND
, OR
, NOT
& DISTINCT
fields are added to allow more complex filtering
{ fruits( filters: { name: "kebab" OR: { name: "raspberry" } } ) { ... }}
Lookups
Lookups can be added to all fields with lookups=True
, which will
add more options to resolve each type. For example:
@strawberry_django.filter(models.Fruit, lookups=True)class FruitFilter: id: auto name: auto
The code above would generate the following schema:
input IDBaseFilterLookup { exact: ID isNull: Boolean inList: [String!]}
input StrFilterLookup { exact: ID isNull: Boolean inList: [String!] iExact: String contains: String iContains: String startsWith: String iStartsWith: String endsWith: String iEndsWith: String regex: String iRegex: String}
input FruitFilter { id: IDFilterLookup name: StrFilterLookup AND: FruitFilter OR: FruitFilter NOT: FruitFilter DISTINCT: Boolean}
Single-field lookup can be annotated with the FilterLookup
generic type.
from strawberry_django import FilterLookup
@strawberry_django.filter(models.Fruit)class FruitFilter: name: FilterLookup[str]
Filtering over relationships
@strawberry_django.filter(models.Color)class ColorFilter: id: auto name: auto
@strawberry_django.filter(models.Fruit)class FruitFilter: id: auto name: auto color: ColorFilter | None
The code above would generate following schema:
input ColorFilter { id: ID name: String AND: ColorFilter OR: ColorFilter NOT: ColorFilter}
input FruitFilter { id: ID name: String color: ColorFilter AND: FruitFilter OR: FruitFilter NOT: FruitFilter}
Custom filter methods
You can define custom filter method by defining your own resolver.
@strawberry_django.filter(models.Fruit)class FruitFilter: name: auto last_name: auto
@strawberry_django.filter_field def simple(self, value: str, prefix) -> Q: return Q(**{f"{prefix}name": value})
@strawberry_django.filter_field def full_name( self, queryset: QuerySet, value: str, prefix: str ) -> tuple[QuerySet, Q]: queryset = queryset.alias( _fullname=Concat( f"{prefix}name", Value(" "), f"{prefix}last_name" ) ) return queryset, Q(**{"_fullname": value})
@strawberry_django.filter_field def full_name_lookups( self, info: Info, queryset: QuerySet, value: strawberry_django.FilterLookup[str], prefix: str ) -> tuple[QuerySet, Q]: queryset = queryset.alias( _fullname=Concat( f"{prefix}name", Value(" "), f"{prefix}last_name" ) ) return strawberry_django.process_filters( filters=value, queryset=queryset, info=info, prefix=f"{prefix}_fullname" )
Warning
It is discouraged to use queryset.filter()
directly. When using more
complex filtering via NOT
, OR
& AND
this might lead to undesired behaviour.
[!TIP]
process_filters
As seen above
strawberry_django.process_filters
function is exposed and can be reused in custom methods. Above it’s used to resolve fields lookupsnull values
By default
null
value is ignored for all filters & lookups. This applies to custom filter methods as well. Those won’t even be called (you don’t have to check forNone
). This can be modified usingstrawberry_django.filter_field(filter_none=True)
This also means that built in
exact
&iExact
lookups cannot be used to filter forNone
andisNull
have to be used explicitly.value resolution
value
parameter of typerelay.GlobalID
is resolved to itsnode_id
attributevalue
parameter of typeEnum
is resolved to is’s value- above types are converted in
lists
as wellresolution can modified via
strawberry_django.filter_field(resolve_value=...)
- True - always resolve
- False - never resolve
- UNSET (default) - resolves for filters without custom method only
The code above generates the following schema:
input FruitFilter { name: String lastName: String simple: str fullName: str fullNameLookups: StrFilterLookup}
Resolver arguments
-
prefix
- represents the current path or position- Required
- Important for nested filtering
- In code bellow custom filter
name
ends up filteringFruit
instead ofColor
without applyingprefix
@strawberry_django.filter(models.Fruit)class FruitFilter: name: auto color: ColorFilter | None
@strawberry_django.filter(models.Color)class ColorFilter: @strawberry_django.filter_field def name(self, value: str, prefix: str): # prefix is "fruit_set__" if unused root object is filtered instead if value: return Q(name=value) return Q()
{ fruits( filters: {color: name: "blue"} ) { ... }}
-
value
- represents graphql field type- Required, but forbidden for default
filter
method - must be annotated
- used instead of field’s return type
- Required, but forbidden for default
-
queryset
- can be used for more complex filtering- Optional, but Required for default
filter
method - usually used to
annotate
QuerySet
- Optional, but Required for default
Resolver return
For custom field methods two return values are supported
- django’s
Q
object - tuple with
QuerySet
and django’sQ
object ->tuple[QuerySet, Q]
For default filter
method only second variant is supported.
What about nulls?
By default null
values are ignored. This can be toggled as such @strawberry_django.filter_field(filter_none=True)
Overriding the default filter
method
Works similar to field filter method, but:
- is responsible for resolution of filtering for entire object
- must be named
filter
- argument
queryset
is Required - argument
value
is Forbidden
@strawberry_django.filter(models.Fruit)class FruitFilter: def ordered( self, value: int, prefix: str, queryset: QuerySet, ): queryset = queryset.alias( _ordered_num=Count(f"{prefix}orders__id") ) return queryset, Q(**{f"{prefix}_ordered_num": value})
@strawberry_django.order_field def filter( self, info: Info, queryset: QuerySet, prefix: str, ) -> tuple[QuerySet, list[Q]]: queryset = queryset.filter( ... # Do some query modification )
return strawberry_django.process_filters( self, info=info, queryset=queryset, prefix=prefix, skip_object_order_method=True )
Tip
As seen above strawberry_django.process_filters
function is exposed and can be
reused in custom methods.
For filter method filter
skip_object_order_method
was used to avoid endless recursion.
Adding filters to types
All fields and CUD mutations inherit filters from the underlying type by default. So, if you have a field like this:
@strawberry_django.type(models.Fruit, filters=FruitFilter)class Fruit: ...
@strawberry.typeclass Query: fruits: list[Fruit] = strawberry_django.field()
The fruits
field will inherit the filters
of the type in the same way as
if it was passed to the field.
Adding filters directly into a field
Filters added into a field override the default filters of this type.
@strawberry.typeclass Query: fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter)
Generic Lookup reference
There is 7 already defined Generic Lookup strawberry.input
classes importable from strawberry_django
BaseFilterLookup
- contains
exact
,isNull
&inList
- used for
ID
&bool
fields
RangeLookup
- used for
range
orBETWEEN
filtering
ComparisonFilterLookup
- inherits
BaseFilterLookup
- additionaly contains
gt
,gte
,lt
,lte
, &range
- used for Numberical fields
FilterLookup
- inherits
BaseFilterLookup
- additionally contains
iExact
,contains
,iContains
,startsWith
,iStartsWith
,endsWith
,iEndsWith
,regex
&iRegex
- used for string based fields and as default
DateFilterLookup
- inherits
ComparisonFilterLookup
- additionally contains
year
,month
,day
,weekDay
,isoWeekDay
,week
,isoYear
&quarter
- used for date based fields
TimeFilterLookup
- inherits
ComparisonFilterLookup
- additionally contains
hour
,minute
,second
,date
&time
- used for time based fields
DatetimeFilterLookup
- inherits
DateFilterLookup
&TimeFilterLookup
- used for timedate based fields
Legacy filtering
The previous version of filters can be enabled via USE_DEPRECATED_FILTERS
Warning
If USE_DEPRECATED_FILTERS is not set to True
legacy custom filtering
methods will be not be called.
When using legacy filters it is important to use legacy
strawberry_django.filters.FilterLookup
lookups as well.
The correct version is applied for auto
annotated filter field (given lookups=True
being set). Mixing old and new lookups
might lead to error DuplicatedTypeName: Type StrFilterLookup is defined multiple times in the schema
.
While legacy filtering is enabled new filtering custom methods are
fully functional including default filter
method.
Migration process could be composed of these steps:
- enable USE_DEPRECATED_FILTERS
- gradually transform custom filter field methods to new version (do not forget to use old FilterLookup if applicable)
- gradually transform default
filter
methods - disable USE_DEPRECATED_FILTERS - This is breaking change