Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

How to extend connection info? #1185

Answered by changeling
oharlem asked this question in Q&A
Discussion options

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!

You must be logged in to vote

For future reference, see if this solution fits your needs:
#636 (comment)

Replies: 16 comments 1 reply

Comment options

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 
You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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 :|

You must be logged in to vote
0 replies
Comment options

@mpmlj

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.

You must be logged in to vote
0 replies
Comment options

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 DjangoConnectionFields and ConnectionFields type() aren't called, in order to get the node's type.

You must be logged in to vote
0 replies
Comment options

@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.

You must be logged in to vote
1 reply
Comment options

Hey, super late but did you manage to find a solution? I'm currently trying to accomplish the same

Comment options

@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?

You must be logged in to vote
0 replies
Comment options

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. :-)

You must be logged in to vote
0 replies
Comment options

@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

You must be logged in to vote
0 replies
Comment options

@aldarund Heh, well, I agree, although I think at least it's readable. There might be something clever I'm missing.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

@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?

You must be logged in to vote
0 replies
Comment options

@greenled what do you mean auto generated connections?

You must be logged in to vote
0 replies
Comment options

@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.

You must be logged in to vote
0 replies
Comment options

For future reference, see if this solution fits your needs:
#636 (comment)

You must be logged in to vote
0 replies
Answer selected by oharlem
Comment options

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
 }
 }
}
You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet
Converted from issue

This discussion was converted from issue #320 on April 14, 2021 20:21.

AltStyle によって変換されたページ (->オリジナル) /