-
Notifications
You must be signed in to change notification settings - Fork 766
How to extend connection info? #1185
-
Hi,
I would appreciate an insight about how to extend connection info with a custom object on the same level as "edges" and "pageInfo". For example please see "custom_object" below for location:
{ "data": { "my_function": { "custom_object": { "field1": 101, "field2": 66, }, "pageInfo": { "endCursor": "foo" }, "edges": [ { "node": { "id": "bar", } }, ... ] } } }
Python 2.7
graphene 2.0
Django 1.10
Thank you!
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
For future reference, see if this solution fits your needs:
#636 (comment)
Replies: 16 comments 1 reply
-
One way to do it for fields at the same level as an edge is to attach an attribute to the node, when you resolve them. And then to resolve for the field at the same level as edges by assessing the attribute.
you can do something similar at the connection level .. I explained this, tho, in more detail to you at graphql-python/graphene#573
class WishConnection(relay.Connection):
class Meta:
node = ProductType
class Edge:
through = graphene.Field(WishReasonType, description='Connection reason')
def resolve_through(self, info, **args):
return self.node.preason[0]
...
# In some Type
def resolve_products(self, info, **args):
qs = Reason.objects.filter(wish=self).only('reason', 'qty')
len = args.get
# notice how I add the preason attribute to each node
result = Paginator(self.products.prefetch_related(
Prefetch('reason_set', queryset=qs, to_attr='preason')).all(),
args.get('first', 20))
return result
Beta Was this translation helpful? Give feedback.
All reactions
-
I haven't actually tried this, but looking at the code for the graphene relay connection, and following it's __init_subclass_with_meta
call, it eventually calls yank_fields_from_attrs
, so I think you can subclass relay.Connection
and add the field as a class attribute.
So I'm thinking this would work?
class ConnectionWithCustomObject(relay.Connection):
custom_object = graphene.Field(MyCustomObject)
class Meta:
node = MyNode
def resolve_custom_object(...)
...
And if it's something you want on all your connections, I think you could have the custom_object stuff on a base class and make all your connections use that. I'd love to hear what ends up working for you.
Beta Was this translation helpful? Give feedback.
All reactions
-
Hi @spockNinja, @japrogramer, Thank you for the advice!
I already have everything setup like this ^, however stuck with a more general picture.
So in brief:
-- currently resolver returns one original queryset
-- In addition to it, I need to pass result of a count operation.
Ideally I would assign it to some variable in the resolver and that variable would be accessible from the my custom connection type.
Example:
tags = relay.ConnectionField(TagConnection) def resolve_tags(self, info, **kwargs): some_count_var = Tag.objects.count() return Tag.objects.get(pk=1)
then
class TagConnection(relay.Connection): class Meta: node = TagNode total_count = graphene.Int() resolve_total_count(self, info): # here I somehow access the `some_count_var` variable and return return some_count_var
hope this all makes sense!
P.S.
@japrogramer , @spockNinja
Apologies for the late response guys :|
Beta Was this translation helpful? Give feedback.
All reactions
-
I can definitely see what you're trying to accomplish. Without whipping up a full example to test, I'm honestly not too sure of the best place to pass that information up, or if it should be passed up at all.
I'm thinking that if something is going to live outside of the edges
, that it needs to be information that does not depend on resolutions inside of edges
. In your example case, with a total Tag.objects.count()
, there is no need to do that in the resolve_tags
part, as you can call the same thing in resolve_total_count
.
If you want some custom information showing up that is dependent on resolutions inside of edges
, then I believe you should go with @japrogramer's suggestion and put it as a custom field on the Edge
of the Connection
and have access to the edge's nodes.
Beta Was this translation helpful? Give feedback.
All reactions
-
Not sure if that solves it, but I just did this to get totalCount back into the a DjangoConnection (without coupling it to an ObjectType):
class CountingConnectionField(graphene_django.DjangoConnectionField): @property def type(self): node_type = super(graphene.relay.ConnectionField, self).type assert issubclass(node_type, graphene_django.DjangoObjectType), "DjangoConnectionField only accepts DjangoObjectType types" class Connection(graphene.Connection): total_count = graphene.Int() class Meta: name = node_type._meta.name + 'Connection' node = node_type def resolve_total_count(self, info, **kwargs): return self.iterable.count() return Connection
The only tricky part in this is to call the super() such that DjangoConnectionField
s and ConnectionField
s type()
aren't called, in order to get the node's type.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2 -
🎉 4
-
@nuschk thank you for this example. I had a similar case where I wanted to add a "conveniences" nodes
field to the connection in order to just get all nodes (ie. much like within the swapi where you have planets
within the allPlanets
connection) and your solution seems to work in my case.
Does this extension have other implications you are aware of? Possible side-effects?
Also, it would be great to have this supported within graphene-django out of the box by just declaring a field and a resolve function on a subclass of DjangoConnectionField.
Beta Was this translation helpful? Give feedback.
All reactions
-
Hey, super late but did you manage to find a solution? I'm currently trying to accomplish the same
Beta Was this translation helpful? Give feedback.
All reactions
-
@nuschk well I ran into a complication with the mentioned approach. I tried to use it for multiple connections and it errors out with AssertionError: Found different types with the same name in the schema: Connection, Connection.
(within graphene/types/typemap.py", line 85, in graphene_reducer
). Did you try it with multiple connections?
Beta Was this translation helpful? Give feedback.
All reactions
-
Well, my brain always melts when I dive into graphene's meta mechanics, so I'm not entirely sure my advice will be sound.
However, the problem is, that the inner Connection
class is declared multiple times. I think the only way you can prevent this, is to declare it for each node type separately. An example with a connection to nodes of type Listing
:
class ListingConnection(graphene.Connection): total_count = graphene.Int() class Meta: node = types.Listing def resolve_total_count(self, info, **kwargs): return self.iterable.count() class CountingListingConnectionField(graphene_django.DjangoConnectionField): @property def type(self): return ListingConnection
Again, please take this with a grain of salt. I don't completely understand what's going on. :-)
Beta Was this translation helpful? Give feedback.
All reactions
-
@nuschk wow. Thats a horrible amount of boilerplate code to simply add total field, especially considering that it should be done for each type...
I wonder if there more friendly solution available
Beta Was this translation helpful? Give feedback.
All reactions
-
@aldarund Heh, well, I agree, although I think at least it's readable. There might be something clever I'm missing.
Beta Was this translation helpful? Give feedback.
All reactions
-
I made the code reusable.
class CountingNodeConnectionField(DjangoFilterConnectionField): def __init__(self, type, fields=None, order_by=None, extra_filter_meta=None, filterset_class=None, *args, **kwargs): self._type = type self._fields = fields self._provided_filterset_class = filterset_class self._filterset_class = None self._extra_filter_meta = extra_filter_meta self._base_args = None super(DjangoFilterConnectionField, self).__init__( type, *args, **kwargs ) @property def type(self): class NodeConnection(graphene.Connection): total_count = graphene.Int() class Meta: node = self._type name = '{}Connection'.format(self._type._meta.name) def resolve_total_count(self, info, **kwargs): return self.iterable.count() return NodeConnection
IMHO But it is not a beautiful.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 5 -
❤️ 3
-
@TheRexx thanks! It works for me with a small change to avoid class names clashing:
name = '{}FilterConnection'.format(self._type._meta.name)
But it works only for those connections declared in the query root, not the auto-generated for each type related list. Any idea of how to cover that case too?
Beta Was this translation helpful? Give feedback.
All reactions
-
@greenled what do you mean auto generated connections?
Beta Was this translation helpful? Give feedback.
All reactions
-
@greenled You can bring this code to a separate module, and connect to the schema where it is needed. In any case, you prescribe nodes for each model manually and they have different parameters. If I did not understand your question correctly, please add a sample code.
Beta Was this translation helpful? Give feedback.
All reactions
-
For future reference, see if this solution fits your needs:
#636 (comment)
Beta Was this translation helpful? Give feedback.
All reactions
-
Solution so that totalCount
field is also discoverable by the schema:
class ExtendedConnection(graphene.relay.Connection): class Meta: abstract = True @classmethod def __init_subclass_with_meta__(cls, node=None, name=None, **options): result = super().__init_subclass_with_meta__(node=node, name=name, **options) cls._meta.fields["total_count"] = graphene.Field( type=graphene.Int, name="totalCount", description="Number of items in the queryset.", required=True, resolver=cls.resolve_total_count, ) return result def resolve_total_count(self, *_) -> int: return self.iterable.count() class Bla(DjangoObjectType): class Meta: connection_class = ExtendedConnection
query {
something() {
edges {
node {
...
}
}
totalCount,
pageInfo {
hasNextPage
}
}
}
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1