strawberry-graphql / strawberry-django

Strawberry GraphQL Django extension
https://strawberry.rocks/docs/django
MIT License
415 stars 120 forks source link

"OR" in filters or Q object in filter #99

Closed henryfool91 closed 1 year ago

henryfool91 commented 2 years ago

Hello everyone, thanks for this great repository, start studying GraphQL with strawberry, and have no regrets so far! I have a question about filters - is there a way to use "or" logic or Django Q object in filters? Intresting about filtering several fields, like

{
 fruits(filters: { name: { iContains: "berry" } OR color: { iContains: "blue" } }) { id name } 
}

Or may be something like

{
 fruits(filters: {Q(name: { iContains: "berry" }) || Q(color: { iContains: "blue" })}) { id name } 
}

Thanks in advance!

Upvote & Fund

Fund with Polar

m4riok commented 1 year ago

Hello @henryfool91 ,

What you are describing can be implemented with the existing api doing something like this:

from django.db.models import Q
from strawberry import UNSET
from strawberry_django.filters import lookup_name_conversion_map 

Z = TypeVar("Z")

def build_or_filter(filters):
    _filter = Q(pk=None)
    fields = getattr(filters, 'fields')
    for _field in fields:
        field = getattr(_field , 'field')
        lookup = getattr(_field, 'lookup')

        for plu in lookup._type_definition.fields:
            _lookup = plu.name
            lu_val = getattr(lookup , _lookup)
            if lu_val is not UNSET:
                if _lookup in lookup_name_conversion_map:
                    _lookup = lookup_name_conversion_map[_lookup]

                _filter |= Q(** { f"{field}__{_lookup}" : f"{lu_val}" })
    return _filter

#Any of these that make sense for your use case
@strawberry.input
class CustomFilterLookup(Generic[Z]):
    exact: Optional[Z] = UNSET
    i_exact: Optional[Z] = UNSET
    contains: Optional[Z] = UNSET
    i_contains: Optional[Z] = UNSET
    in_list: Optional[List[Z]] = UNSET
    gt: Optional[Z] = UNSET
    gte: Optional[Z] = UNSET
    lt: Optional[Z] = UNSET
    lte: Optional[Z] = UNSET
    starts_with: Optional[Z] = UNSET
    i_starts_with: Optional[Z] = UNSET
    ends_with: Optional[Z] = UNSET
    i_ends_with: Optional[Z] = UNSET
    range: Optional[List[Z]] = UNSET
    is_null: Optional[bool] = UNSET
    regex: Optional[str] = UNSET
    i_regex: Optional[str] = UNSET

@strawberry.input
class FieldWithFilter(Generic[Z]):
    field: str
    lookup: CustomFilterLookup[Z]

@strawberry.input
class FieldsOrFilter(Generic[Z]):
    fields: List[ FieldWithFilter[Z]]

@strawberry.django.filters.filter(MyModel,lookups=True)
class MyModelFilter:
    model_field: auto
    other_model_field: auto 
    fields_or_filter: FieldsOrFilter[str]

    def filter_fields_or_filter(self,queryset):
        _filter = build_or_filter(self.fields_or_filter)
        return queryset.filter(_filter)

You can then query normally with the 3 fields joined together with AND and the internals of fields_or_filter with OR. Something like this:

{
  myField(
    filters: {
      fieldsOrFilter: {
        fields : [ 
           { 
             field: "some_model_field", 
             lookup: { iContains: "ska"}
           }, 
           {
             field: "some_other_model_field", 
             lookup: {iContains: "ATA"}
           }
        ] 
      },
      model_field: { gt: 100 },
      other_model_field: { lte: 40 }
    }
) {
    ...myFieldFragment 
}

@bellini666 , or anyone who might know. Is this already somehow implemented into the library and I missed it? If I am reinventing the wheel here please give me a heads up. Could not find anything like this in the docs or issues here. Just a PR proposal describing something like this. Otherwise I would love to see something like this included in future versions.

bellini666 commented 1 year ago

@m4riok this is not implemented in the lib. The only problem I see there is that it totally transforms the AND in OR, so you have to choose between one and another. It is not possible to do something more complex like foo AND (bar OR baz)

To be able to handle more complex cases I think we will need to do a major refactor of the implementation, with possible breaking changes

m4riok commented 1 year ago

@bellini666 it only transforms AND to OR for everything defined inside fieldsOrFilter. The model_field and other_model_field in the above example retain their AND logic and are joined with fieldsOrFilter via AND. The example above would produce the following statement:

    Q(model_field__gt=100) & 
    Q(other_model_field__lte=40) & 
    ( Q(some_other_model_field__icontains='ATA') | Q(some_model_field__icontains='ska')

And if you want to have multiple OR blocks joined together with AND logic along with the already provided lookups for the model fields you could define multiple instances of FieldsOrFilter[Generic[T]] like so:

@strawberry.django.filters.filter(MyModel,lookups=True)
class MyModelFilter:
    model_field: auto
    other_model_field: auto 
    fields_or_filter: FieldsOrFilter[str]
    another_or_filter: FieldsOrFilter[int]

    def filter_fields_or_filter(self,queryset):
        _filter = build_or_filter(self.fields_or_filter)
        return queryset.filter(_filter)

    def filter_another_or_filter(self,queryset):
        _filter = build_or_filter(self.another_or_filter)
        return queryset.filter(_filter)

Or it all can be made more generic like this:

@strawberry.input
class FieldsOrFilter(Generic[Z]):
    fields: List[ FieldWithFilter[Z]]
    logic: str

@strawberry.input
class MultipleNestedOrFiltersWithSelectableLogic(Generic[Z]):
    blocks: List[FieldsOrFilter[Z]]

And just introduce a resolver for this filter type that would add another loop to unpack the blocks list and apply the logic selected by the user between them

The only bad thing about this is that it allows for a single level of nesting. You cannot do overly complex logic like X and Y and ( Z or ( not K and not M)) or anything with more than one level of nested statements.

But imho supporting more than 1 level of nested expressions in filters provided by the library is sort of an overkill. Most people having a need for such functionality could implement their own filter resolver. But a block of OR statements joined via AND with the existing model field lookups is a very common use case.

bellini666 commented 1 year ago

This was fixed in https://github.com/strawberry-graphql/strawberry-graphql-django/releases/tag/v0.14.0 :)