Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enable PK based filtering in Django Graphene Relay while retaining Global IDs

Problem

I am using django-graphene with Relay on our GraphQL Server. The implementation imposes a Global ID requirement in the graphene.relay.Node class that overrides and hides Django's ID field.

As a result, I can query like this:

{
    allBatches(id:"QmF0Y2hOb2RlOjE=") {
    edges {
      node {
        id
        pk
      }
    }
  }
}

And get this response:

{
  "data": {
    "allBatches": {
      "edges": [
        {
          "node": {
            "id": "QmF0Y2hOb2RlOjE=",
            "pk": 1
          }
        }
      ]
    }
  }
}

However, what I lose is the ability to filter by the original ID (or PK) field of the Object itself:

{
    allBatches(id:1) {
    edges {
      node {
        id
        pk
      }
    }
  }
}

In fact, I simply cannot filter objects by ID. I can think of two possible work-arounds to this: 1. Prevent django-graphene-relay from hijacking and shadowing the id field, perhaps force it to use a different field name such as gid 2. Find a way to include pk as a special field that is available both as a property and in filter

Solution 1

I have made no progress on 1 since it appears as though django-graphene (and perhaps the relay standard) imposes a limitation that this field be called id. I see that id has been used as a Magic String in multiple places and there does not appear to be a standard way to change the field name.

Solution 2

On 2, I can get the property to work with a Mixin like this:

class PKMixin(object):
    pk = graphene.Field(type=graphene.Int, source='pk')

However, I am unable to get the filtering via django-filter to work, since the FilterSet does not have the field pk declared and breaks with the following error

'Meta.fields' contains fields that are not defined on this FilterSet: pk

Update on 2

I tried the following:

class PKFilteringNode(Node):

    @classmethod
    def get_node_from_global_id(cls, info, global_id, only_type=None):
        # So long as only_type is set; if we detect that the global_id is a pk and not a global ID;
        # then coerce it to be a proper global ID before fetching
        if only_type:
            try:
                int(global_id)
                global_id = cls.to_global_id(only_type._meta.name, global_id)
                return super(PKFilteringNode, cls).get_node_from_global_id(info, global_id, only_type)
            except ValueError:
                pass
        return super(PKFilteringNode, cls).get_node_from_global_id(info, global_id, only_type)

And now I can get GraphQL to do this:

{
  batchA: batch(id: "QmF0Y2hOb2RlOjE=") {
    id
    name
  }
  batchB: batch(id: 1) {
    id
    name
  }
}
{
  "data": {
    "batchA": {
      "id": "QmF0Y2hOb2RlOjE=",
      "name": "Default Batch"
    },
    "batchB": {
      "id": "QmF0Y2hOb2RlOjE=",
      "name": "Default Batch"
    }
  }
}

But I have a fairly strong fear this will break something downstream, at the level of caching perhaps? Also this does not allow filtering by ID still since filtering depends on DjangoFilterConnectionField

Request

I am stuck at the moment. I have a few questions:

  1. Is this an unusual requirement to begin with? Am I asking the wrong question when I wish to retain the ability to filter by pk
  2. Is there a standard pattern to solve this problem?

Related Issue on Github

https://github.com/graphql-python/graphene-django/issues/349

Versions

  • graphene-django==2.1.0
  • django==1.9.12
  • django-filter==1.0.1
  • python==2.7.13
like image 419
rtindru Avatar asked Jan 23 '19 13:01

rtindru


1 Answers

I am not sure that you still want the answer or not, but at least let me try to answer to your question. Correct if my understanding is wrong. I just willing to help

Actually pk supposed to be DetailView not ListView that being used with filter.

requirements.txt

graphene-django==2.7.1
django==3.0.1
django-filter==2.2.0
python==3.8.1

models.py

from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()


class Objection(models.Model):
    detail = models.TextField(null=True, blank=True)
    hidden = models.BooleanField(default=False)
    report = models.BooleanField(default=False)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='objections',
                                   related_query_name='objection')

nodes.py

import django_filters
import graphene
from graphene import relay
from graphene_django import DjangoObjectType

from multy_herr.objections.models import Objection


class ObjectionFilter(django_filters.FilterSet):
    pk = django_filters.NumberFilter(field_name='pk')

    class Meta:
        model = Objection
        fields = [
            'pk',
        ]


class ObjectionNode(DjangoObjectType):
    pk = graphene.Field(type=graphene.Int, source='id')

    class Meta:
        model = Objection
        fields = [
            'id',
            'pk',
            'detail',
            'hidden',
            'report',
        ]
        filter_fields = {
            'pk': ['exact'],
            'detail': ['icontains', 'istartswith'],
            'created_by__name': ['icontains', ],
            'hidden': ['exact'],
            'report': ['exact'],
        }
        interfaces = (relay.Node,)


queries.py

import graphene
from graphene import relay
from graphene_django.filter import DjangoFilterConnectionField

from multy_herr.objections.grapheql.nodes import ObjectionNode, ObjectionFilter
from multy_herr.objections.models import Objection


class ObjectionQuery(graphene.ObjectType):
    objection = relay.Node.Field(ObjectionNode)
    all_objections = DjangoFilterConnectionField(ObjectionNode,
                                                 filterset_class=ObjectionFilter)

    def resolve_all_objections(self, info, **kwargs):
        if info.context.user.is_authenticated is False:
            return Objection.objects.none()
        return Objection.objects.filter(created_by=info.context.user)

I leave comment in query here for analogy. With my hackish solution Insomnia application will warns me with Unknown argument pk .... But works

query

query{
#   objection(id: "T2JqZWN0aW9uTm9kZTo1"){
#     id
#     report
#     hidden
#   }
  allObjections(pk: 5){
    edges{
      node{
        id
        pk
        hidden
        report
      }
    }
  }
}

response

{
  "data": {
    "allObjections": {
      "edges": [
        {
          "node": {
            "id": "T2JqZWN0aW9uTm9kZTo1",
            "pk": 5,
            "hidden": false,
            "report": false
          }
        }
      ]
    }
  }
}
like image 153
joe Avatar answered Sep 24 '22 13:09

joe