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

2

20

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
Epley answered 23/1, 2019 at 13:43 Comment(5)
The API always returns the node with the derived Global ID. As a client, if I need to lookup the node, I can utilize this same identifier. Exposing the underlying PK seems unnecessary unless A) there's some other service you're interacting with that uses the PK as a reference or B) the PK is otherwise significant to the client from a business-rules perspective (i.e. the client compares PK values to enforce some business logic). Why do you feel like exposing the PK is necessary?Lassa
Legacy code that is tightly coupled to PKsEpley
Basically, both A) and B) are true for us.Epley
website URLs with IDs in are a legitimate use case.Pouncey
I considered using graphene, but this is such a strong limitation I'll probably need to move away from the ideaPsychogenic
S
4

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
          }
        }
      ]
    }
  }
}
Stockist answered 21/12, 2019 at 12:18 Comment(0)
P
0

Have you tried solution 2 but with using id as the source instead?

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

Also, if you're only looking to fetch a single record, you shouldn't go via a connection field anyway. You should define something like a batchByPk field on your schema instead.

A final thing to be aware of, is that at the current time graphene-django's DjangoFilterConnectionField isn't implemented in an efficient way, so you may not even want to use it anyway.

Pouncey answered 24/1, 2019 at 10:21 Comment(3)
Can you tell me more about how DjangoFilterConnectionField is inefficient? Is there documentation or issues on Github I can read to find out more?Epley
source='id' does not help - same error: >'Meta.fields' contains fields that are not defined on this FilterSet: pk This error persists unless an explicit FilterSet is declared with pk.Epley
@Epley at the moment, all of the connection code in Graphene implements limit/offset pagination rather than true cursor-based pagination, so it doesn't have any of the true benefit of the pattern -- it's just faking it.Pouncey

© 2022 - 2024 — McMap. All rights reserved.