How to filter a query by a list of ids in GraphQL using graphene-django?
Asked Answered
W

5

8

I'm trying to perform a GraphQL query using Django and Graphene. To query one single object using the id I did the following:

{
  samples(id:"U2FtcGxlU2V0VHlwZToxMjYw") {
    edges {
      nodes {
        name
      }
    }
  }
}

And it just works fine. Problem arise when I try to query with more than one id, like the following:

{
  samples(id_In:"U2FtcGxlU2V0VHlwZToxMjYw, U2FtcGxlU2V0VHlwZToxMjYx") {
    edges {
      nodes {
        name
      }
    }
  }
} 

In the latter case I got the following error:

argument should be a bytes-like object or ASCII string, not 'list'

And this is a sketch of how defined the Type and Query in django-graphene

class SampleType(DjangoObjectType):
  class Meta:
    model = Sample
    filter_fields = {
      'id': ['exact', 'in'],
     }
     interfaces = (graphene.relay.Node,)

class Query(object):
  samples = DjangoFilterConnectionField(SampleType)

  def resolve_sample_sets(self, info, **kwargs):
    return Sample.objects.all()
Wits answered 16/1, 2019 at 13:49 Comment(2)
First, you need to extend SampleType with a list field -- something like ids = graphene.List(graphene.ID())Polyphemus
@MarkChackerian could you elaborate on this and provide a solution? I've run into the same issue and can't see how adding the ids field changes anything. It seems graphene-django has some input validation issues on the in field?Barquentine
F
6

GlobalIDMultipleChoiceFilter from django-graphene kinda solves this issue, if you put "in" in the field name. You can create filters like

from django_filters import FilterSet
from graphene_django.filter import GlobalIDMultipleChoiceFilter

class BookFilter(FilterSet):
    author = GlobalIDMultipleChoiceFilter()

and use it by

{
  books(author: ["<GlobalID1>", "<GlobalID2>"]) {
    edges {
      nodes {
        name
      }
    }
  }
}

Still not perfect, but the need for custom code is minimized.

Fourflusher answered 21/3, 2020 at 5:11 Comment(0)
E
3

You can easily use a Filter just put this with your nodes.

class ReportFileFilter(FilterSet):
    id = GlobalIDMultipleChoiceFilter()

Then in your query just use -

class Query(graphene.ObjectType):
    all_report_files = DjangoFilterConnectionField(ReportFileNode, filterset_class=ReportFileFilter)

This is for relay implementation of graphql django.

Everybody answered 3/5, 2020 at 3:58 Comment(0)
B
2

I had trouble implementing the 'in' filter as well--it appears to be misimplemented in graphene-django right now and does not work as expected. Here are the steps to make it work:

  1. Remove the 'in' filter from your filter_fields
  2. Add an input value to your DjangoFilterConnectionField entitled 'id__in' and make it a list of IDs
  3. Rename your resolver to match the 'samples' field.
  4. Handle filtering by 'id__in' in your resolver for the field. For you this will look as follows:
from base64 import b64decode

def get_pk_from_node_id(node_id: str):
    """Gets pk from node_id"""
    model_with_pk = b64decode(node_id).decode('utf-8')
    model_name, pk = model_with_pk.split(":")
    return pk


class SampleType(DjangoObjectType):
    class Meta:
        model = Sample
        filter_fields = {
            'id': ['exact'],
         }
        interfaces = (graphene.relay.Node,)


class Query(object):

    samples = DjangoFilterConnectionField(SampleType, id__in=graphene.List(graphene.ID))

    def resolve_samples(self, info, **kwargs):
        # filter_field for 'in' seems to not work, this hack works
        id__in = kwargs.get('id__in')
        if id__in:
            node_ids = kwargs.pop('id__in')
            pk_list = [get_pk_from_node_id(node_id) for node_id in node_ids]
            return Sample._default_manager.filter(id__in=pk_list)
        return Sample._default_manager.all()

This will allow you to call the filter with the following api. Note the use of an actual array in the signature (I think this is a better API than sending a comma separated string of values). This solution still allows you to add other filters to the request and they will chain together correctly.

{
  samples(id_In: ["U2FtcGxlU2V0VHlwZToxMjYw", "U2FtcGxlU2V0VHlwZToxMjYx"]) {
    edges {
      nodes {
        name
      }
    }
  }
} 
Barquentine answered 13/8, 2019 at 22:53 Comment(0)
H
2

None of the existing answers seemed to work for me as they were presented, however with some slight changes I managed to resolve my problem as follows:

You can create a custom FilterSet class for your object type, and filter the field by using the GlobalIDMultipleChoiceFilter. for example:

from django_filters import FilterSet
from graphene_django.filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter

class SampleFilter(FilterSet):
    id = GlobalIDFilter()
    id__in = GlobalIDMultipleChoiceFilter(field_name="id")

    class Meta:
        model = Sample
        fields = (
            "id_in",
            "id",
        )

Something I came cross is that you can not have filter_fields defined with this approach. Instead, you have to only rely on the custom FilterSet class exclusively, making your object type effectively look like this:

from graphene import relay
from graphene_django import DjangoObjectType

class SampleType(DjangoObjectType):
    class Meta:
        model = Sample
        filterset_class = SampleFilter
        interfaces = (relay.Node,)
Hulbard answered 7/6, 2020 at 14:44 Comment(0)
C
0

Another way is to tell the Relay filter of graphene_django to also deals with a list. This filter is register in a mixin in graphene_django and applied to any filter you define.

So here my solution:

from graphene_django.filter.filterset import (
    GlobalIDFilter,
    GrapheneFilterSetMixin,
)
from graphql_relay import from_global_id


class CustomGlobalIDFilter(GlobalIDFilter):
    """Allow __in lookup for IDs"""
    def filter(self, qs, value):
        if isinstance(value, list):
            value_lst = [from_global_id(v)[1] for v in value]
            return super(GlobalIDFilter, self).filter(qs, value_lst)
        else:
            return super().filter(qs, value)

# Fix the mixin defaults
GrapheneFilterSetMixin.FILTER_DEFAULTS.update({
    AutoField: {"filter_class": CustomGlobalIDFilter},
    OneToOneField: {"filter_class": CustomGlobalIDFilter},
    ForeignKey: {"filter_class": CustomGlobalIDFilter},
})
Caryopsis answered 6/11, 2020 at 16:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.