Skip to content

How to have edges/totalCount on a relationship field? #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rok-povsic opened this issue Jul 27, 2017 · 20 comments
Closed

How to have edges/totalCount on a relationship field? #58

rok-povsic opened this issue Jul 27, 2017 · 20 comments

Comments

@rok-povsic
Copy link

Hey,

I'd like to have edges/node/totalCount support on a relationship field.

E.g. if looking at the flask_alchemy example app, I'd like to have query such as this (changes are in bold):

{
  allEmployees {
    edges {
      node {
        id,
        name,
        department {
          totalCount
          edges {
              id,
              name
           }
        },
        role {
          totalCount
          edges {
            id,
            name
           }
        }
      }
    }
  }
}

Can this be done?

Thank you

@shanmugh
Copy link

+1

@shanmugh
Copy link

Also, I tried the work-around suggested here - graphql-python/graphene#151 by using https://pypi.python.org/pypi/graphene-sqlalchemy/2.0.dev2017072601 but the Connection object appears to be missing the length field.

I had to fork from graphene-sqlalchemy-1.1.1 and add this patch (d66aa07) to get it working.

@singingwolfboy
Copy link

@shanmugh, can you make a pull request to add your modification to graphene-sqlalchemy? Or post a working example, so that someone else can do so? I'd be happy to make that pull request for you, if you'd like.

@nmrtv
Copy link

nmrtv commented Sep 13, 2017

@singingwolfboy, that modification (length) is already added, but if you need totalCount in your connection, you must inherit Connection class. Because totalCount is not in relay specification and not everyone needs it.

@singingwolfboy
Copy link

@neriusmika Can you show me how to do that? This is the code I have so far, and it doesn't seem to work...

import graphene
from graphene.relay import Node
from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
from myapp import models


class CountableConnection(SQLAlchemyConnectionField):
    total_count = graphene.Int()

    def resolve_total_count(self, info):
        return self.length


class Contact(SQLAlchemyObjectType, interfaces=[Node]):
    class Meta:
        model = models.Contact


class User(SQLAlchemyObjectType, interfaces=[Node]):
    class Meta:
        model = models.User
        
    contacts = CountableConnection(Contact)

@nmrtv
Copy link

nmrtv commented Sep 13, 2017

You need to inherit CountableConnection from graphene.relay.Connection, not from SQLAlchemyConnectionField. But there is a problem to pass that connection to your Contact class. It has connection parameter, but now it's useless, because Connection class needs node parameter and you will have circular reference. Probably the simplest way would be to reassign graphene.relay.Connection (look at #65 (comment)), but then it will be used in all your connections. The other way (which I prefer) is to change SQLAlchemyObjectType implementation.

@singingwolfboy
Copy link

@neriusmika That's helpful, but do you think you could provide an actual code example? I find it's easier to refer to an example, and tweak it from there.

@nmrtv
Copy link

nmrtv commented Sep 14, 2017

Based on your example, you can try this code, but I did not check it:

import graphene
from graphene.relay import Node
from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
from myapp import models


class CountableConnection(graphene.relay.Connection):
    class Meta:
        abstract = True

    total_count = graphene.Int()

    @staticmethod
    def resolve_total_count(root, info):
        return root.length

graphene.relay.Connection = CountableConnection


class Contact(SQLAlchemyObjectType, interfaces=[Node]):
    class Meta:
        model = models.Contact


class User(SQLAlchemyObjectType, interfaces=[Node]):
    class Meta:
        model = models.User
        

class Query(graphene.ObjectType):
    node = Node.Field()

    all_users = SQLAlchemyConnectionField(User)

@singingwolfboy
Copy link

That seems to work. Thank you for the help!

@sebastiandev
Copy link

sebastiandev commented Dec 4, 2017

Im looking for the same, but this approach is not working for me.
The relay.Connection is not been replaced, is still the base Connection. How can I specify that I want to use another Connection class?

I ended up subclassing SQLAlchemyObjectType to force it to use the CountableConnecition, like this:

class CountableConnection(graphene.relay.Connection):
    class Meta:
        abstract = True

    total_count = graphene.Int()

    @staticmethod
    def resolve_total_count(root, info, *args, **kwargs):
        return root.length


class CustomSQLAlchemyObjectType(SQLAlchemyObjectType):

    class Meta:
        abstract = True

    @classmethod
    def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
                                    only_fields=(), exclude_fields=(), connection=None,
                                    use_connection=None, interfaces=(), id=None, **options):
        # Force it to use the countable connection
        countable_conn = connection or CountableConnection.create_type(
            "{}CountableConnection".format(model.__name__),
            node=cls)

        super(CustomSQLAlchemyObjectType, cls).__init_subclass_with_meta__(
            model, 
            registry, 
            skip_registry,
            only_fields,
            exclude_fields, 
            countable_conn,
            use_connection, 
            interfaces, 
            id,
            **options)

Thanks

@CaselIT
Copy link
Contributor

CaselIT commented Dec 18, 2017

I've added a PR to fix this issue here #104

@docelic
Copy link

docelic commented Oct 9, 2019

Here is a complete working example which works without modifying graphene.relay.Connection globally, and which uses the features present in graphene-sqlalchemy and graphene-sqlalchemy-filters to provide "sort" and "filters" options on all connection fields where that is desired, regardless of whether the fields are manually defined or are auto-detected and auto-exposed by graphql-sqlalchemy:

  1. Necessary helper functions:
import graphene
import graphene_sqlalchemy
from sqlalchemy import and_, or_
from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType
from graphene_sqlalchemy_filter import FilterableConnectionField, FilterSet

# Based on object class, returns the appropriate filter class
def filter_for(model_type):
  if model_type == GModelA:
    return(GModelAFilter())
  elif model_type == GModelB:
    return(GModelBFilter())
  else:
    raise(Exception('Unknown model_type %s; extend filter_for() to solve it' % str(model_type)))

# Creates connection field using FilterableConnectionField rather than the default SQLAlchemyConnectionField
def field_factory(relationship, registry, **field_kwargs):
    model = relationship.mapper.entity
    model_type = registry.get_type_for_model(model)
    return FilterableConnectionField(model_type._meta.connection, filters=filter_for(model_type), **field_kwargs)

# Connection subclass which adds totalCount field
class GCountedConnection(graphene.relay.Connection):
  class Meta:
    abstract = True

  total_count = graphene.Field(graphene.NonNull(graphene.Int))
  def resolve_total_count(s,i, **kwargs):
    return(s.length)
  1. Model's filter definition and GraphQL definition:
class GModelAFilter(FilterSet):
    class Meta:
        model = ModelA
        # Fields which can be searched, and with which operators
        fields = {
            'name': ['eq','ne','like','ilike','in','not_in','contains'],
        }
class GModelA(graphene_sqlalchemy.SQLAlchemyObjectType):
  class Meta:
    model = ModelA
    #interfaces = (graphene.relay.Node, )
    connection_class = GCountedConnection
    connection_field_factory = field_factory

With this, all conections you define in this way will automatically and uniformly have the appropriate sort: and filters: options.

query {
  modelb(id: ...) {
    modelAs(filters: ..., sort: ...) {
      totalCount
      edges {
        ...returns
      }
    }
  }

).

  1. And if you want to retrieve ModelAs from the root of the tree, then either define "model_as" on the root object (if you have one set up), or manually define the field and provide its resolver, e.g.:
class GRoot(graphene.ObjectType):
  model_as = FilterableConnectionField(GModelA._meta.connection, filters=filter_for(GModelA))

schema = graphene.Schema(
  query=GRoot,
  auto_camelcase=True,
)

(In the above, using ._meta.connection is needed to also get the sort option automatically.)

query {
  modelAs(sort: ..., filters: ...) {
    totalCount
    edges {
      ...
    }
  }
}

If you happen to receive error object of type 'SQLAlchemyConnectionField' has no len(), see #236.

  1. Notes:

If you would want to disable automatic wrapping of relationships into connections, so that querying modelAs would return a plain list rather than a connection, then you would specify use_connection = False instead of connection_class = ....

@gu3sss
Copy link

gu3sss commented Aug 6, 2020

I have been trying the above suggestion and struggling to succeed. Constantly getting the below,
Cannot query field \"totalCount\" on type \"TestConnection

@AlexEshoo
Copy link

AlexEshoo commented Aug 7, 2020

I had the same requirement to add a totalCount and edgeCount to the connection object, but also I needed to add a convenience field called nodes to the top level of the connection to return the edges without cursors and deep nesting. I ended up achieving it by sub-classing SQLAlchemyConnectionField:

class ExtendedSQLAlchemyConnectionField(SQLAlchemyConnectionField):
    @property
    def type(self):
        connection_type = super().type
        node_type = connection_type._meta.node  # !!!!

        # We have to define the class at run time in a closure to access the specific `node_type` class
        # of the ConnectionField to be able to supply the convenience field `nodes` at the top level of
        # the connection object.
        class ExtendedConnection(relay.Connection):
            class Meta:
                node = node_type

            total_count = graphene.Int()
            edge_count = graphene.Int()
            nodes = graphene.List(node_type)  # Inject the specific `node` class Field Type

            @staticmethod
            def resolve_total_count(root, info):
                return root.length

            @staticmethod
            def resolve_edge_count(root, info):
                return len(root.edges)

            @staticmethod
            def resolve_nodes(root, info):
                return [edge.node for edge in root.edges]

        return ExtendedConnection

This lets me define my types in the normal way:

class Dog(SQLAlchemyObjectType):
    class Meta:
        model = DogModel
        interfaces = (relay.Node,)

And my query:

class Query(graphene.ObjectType):
    all_dogs = ExtendedSQLAlchemyConnectionField(Dog.connection)

and run queries like:

query {
    allDogs {
        totalCount
        edgeCount
        nodes {
            id
        }
        edges {
            cursor
            node {
                id
            }
        }
    }
}

Not sure if this is the best approach, but it works and if it saves someone else from trying to understand the inner workings of graphene, I'm glad to share it.

@gu3sss
Copy link

gu3sss commented Aug 7, 2020

@AlexEshoo Thanks a lot sharing. I was able to finally able to get it to working this morning. Nonetheless, it seems like the value of total count I get is actually before Graphql does a DISTINCT on the resultset. To be clear, if my query fetched 50 matching records from my db and only 20 are unique, I see 50 in total count but only get 20 unique records in result. Is there a way to calculate length of unique root instead of root.length ?

@avbentem
Copy link

@AlexEshoo,

I ended up achieving it by sub-classing SQLAlchemyConnectionField
...
This lets me define my types in the normal way:

class Dog(SQLAlchemyObjectType):
    class Meta:
        model = DogModel
        interfaces = (relay.Node,)

...

class Query(graphene.ObjectType):
   all_dogs = ExtendedSQLAlchemyConnectionField(Dog.connection)

Very nice! However, @neriusmika's simpler approach in #58 (comment) of defining just a custom standalone CountableConnection , along with (repeatedly 😢) declaring its usage in, e.g.:

class Dog(SQLAlchemyObjectType):
    class Meta:
        model = DogModel
        interfaces = (relay.Node,)
        # explicitly define the connection
        connection_class = CountableConnection

...and the standard:

class Query(graphene.ObjectType):
    all_dogs = SQLAlchemyConnectionField(Dog.connection)
    all_employees = SQLAlchemyConnectionField(Employee.connection)

...seems to have the added benefit of providing totalCount in nested connections as well? Like:

query {
  allEmployees {
    # totalCount supported with both ExtendedSQLAlchemyConnectionField
    # and connection_class=CountableConnection
    totalCount
    edges {
      node {
        name
        dogs {
          # totalCount not supported with ExtendedSQLAlchemyConnectionField,
          # but supported with connection_class=CountableConnection
          totalCount
          edges {
            node {
              name
            }
          }
        }
      }
    }
  }
}

Maybe this won't work for top-level convenience helpers. But for just totalCount it's surely nice to have it available in nested connections as well.

(All thanks for the examples!)

@himat
Copy link

himat commented Nov 13, 2020

This didn't work for me, I get AssertionError: The type X doesn't have a connection

@ShantanuJoshi
Copy link

ShantanuJoshi commented Nov 17, 2020

This didn't work for me, I get AssertionError: The type X doesn't have a connection

Dude figured this out you need the beta/newest version: #272

@erikwrede
Copy link
Member

Closing this as the implementation of such a feature seems to be possible. It might be handy to add the recipe presented here to the docs/examples. I'll link this in a separate issue. If you feel that this should be a default feature instead of a manual recipe, please comment there :)

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topics referencing this issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests