File: filters.md

package info (click to toggle)
strawberry-graphql-django 0.62.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,968 kB
  • sloc: python: 27,530; sh: 17; makefile: 16
file content (480 lines) | stat: -rw-r--r-- 12,813 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
---
title: Filtering
---

# Filtering

It is possible to define filters for Django types, which will
be converted into `.filter(...)` queries for the ORM:

```python title="types.py"
import strawberry_django
from strawberry import auto

@strawberry_django.filter_type(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:

```graphql title="schema.graphql"
input FruitFilter {
  id: ID
  name: String
  AND: FruitFilter
  OR: FruitFilter
  NOT: FruitFilter
  DISTINCT: Boolean
}
```

> [!TIP]
> If you are using the [relay integration](relay.md) 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](./settings.md)
> 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

```graphql
{
  fruits(
    filters: {
      name: "kebab"
      OR: {
        name: "raspberry"
      }
    }
  ) { ... }
}
```

## List-based AND/OR/NOT Filters

The `AND`, `OR`, and `NOT` operators can also be declared as lists, allowing for more complex combinations of conditions. This is particularly useful when you need to combine multiple conditions in a single operation.

```python title="types.py"
@strawberry_django.filter_type(models.Vegetable, lookups=True)
class VegetableFilter:
    id: auto
    name: auto
    AND: Optional[list[Self]] = strawberry.UNSET
    OR: Optional[list[Self]] = strawberry.UNSET
    NOT: Optional[list[Self]] = strawberry.UNSET
```

This enables queries like:

```graphql
{
  vegetables(
    filters: {
      AND: [{ name: { contains: "blue" } }, { name: { contains: "squash" } }]
    }
  ) {
    id
  }
}
```

The list-based filtering system differs from the single object filter in a few ways:

1. It allows combining multiple conditions in a single `AND`, `OR`, or `NOT` operation
2. The conditions in a list are evaluated together as a group
3. When using `AND`, all conditions in the list must be satisfied
4. When using `OR`, any condition in the list can be satisfied
5. When using `NOT`, none of the conditions in the list should be satisfied

This is particularly useful for complex queries where you need to have multiple conditions against the same field.

## Lookups

Lookups can be added to all fields with `lookups=True`, which will
add more options to resolve each type. For example:

```python title="types.py"
@strawberry_django.filter_type(models.Fruit, lookups=True)
class FruitFilter:
    id: auto
    name: auto
```

The code above would generate the following schema:

```graphql title="schema.graphql"
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.

```python title="types.py"
from strawberry_django import FilterLookup

@strawberry_django.filter(models.Fruit)
class FruitFilter:
    name: FilterLookup[str]
```

## Filtering over relationships

```python title="types.py"
@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:

```graphql title="schema.graphql"
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.

```python title="types.py"
@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 lookups
>
> #### null 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 for `None`).
> This can be modified using
> `strawberry_django.filter_field(filter_none=True)`
>
> This also means that built in `exact` & `iExact` lookups cannot be used to filter for `None`
> and `isNull` have to be used explicitly.
>
> #### value resolution
>
> - `value` parameter of type `relay.GlobalID` is resolved to its `node_id` attribute
> - `value` parameter of type `Enum` is resolved to is's value
> - above types are converted in `lists` as well
>
> resolution 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:

```graphql title="schema.graphql"
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 filtering `Fruit` instead of `Color` without applying `prefix`

```python title="Why prefix?"
@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()
```

```graphql
{
  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
- `queryset` - can be used for more complex filtering
  - Optional, but **Required** for default `filter` method
  - usually used to `annotate` `QuerySet`

#### Resolver return

For custom field methods two return values are supported

- django's `Q` object
- tuple with `QuerySet` and django's `Q` 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**

```python title="types.py"
@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:

```python title="types.py"
@strawberry_django.type(models.Fruit, filters=FruitFilter)
class Fruit:
    ...

@strawberry.type
class 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.

```python title="schema.py"
@strawberry.type
class 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` or `BETWEEN` 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**](settings.md#strawberry_django)

> [!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_**