title: Django Graphene return data from multiple models under the same nest
body: <p>I am trying to use Python Graphene GraphQL to implement a search endpoint return all products based on name. However, in my database I have three product tables that respectfully contains different product types - Cards, Tokens, Sealed Products.</p>
  <p>I want to return the data under a single nest in the Json response. The relay connection I am using is from <a href="https://github.com/saltycrane/graphene-relay-pagination-example/blob/artsy-example/README.md" rel="nofollow noreferrer">https://github.com/saltycrane/graphene-relay-pagination-example/blob/artsy-example/README.md</a>.</p>
  <p>Something along the lines of:</p>
  <p><strong>Code</strong>:</p>
  <pre><code>import graphene
  from django.db.models import Q
  from graphene import relay, ObjectType
  from graphene_django import DjangoObjectType
  
  from magic.models import magic_sets_cards, magic_sets_tokens
  from magic.pagination.fields import ArtsyConnection, ArtsyConnectionField
  
  class MagicCards(DjangoObjectType):
      id = graphene.ID(source='pk', required=True)
      mana_cost_list = graphene.List(graphene.String)
  
      class Meta:
          model = magic_sets_cards
          interfaces = (relay.Node,)
          filter_fields = {'name': ['icontains']}
          connection_class = ArtsyConnection
  
  
  class MagicTokens(DjangoObjectType):
      id = graphene.ID(source='pk', required=True)
  
      class Meta:
          model = magic_sets_tokens
          interfaces = (relay.Node,)
          filter_fields = {'name': ['icontains']}
          connection_class = ArtsyConnection
  
  
  class SearchQuery(ObjectType):
      magic_cards = ArtsyConnectionField(MagicCards)
      magic_tokens = ArtsyConnectionField(MagicTokens)
      # pseudo code:
      all_products = combine(magic_cards, magic_tokens)
  
      @staticmethod
      def resolve_all_products(self, info, **kwargs):
          return
  
      @staticmethod
      def resolve_magic_cards(self, info, **kwargs):
          sql_number_to_int = &quot;CAST((REGEXP_MATCH(number, '\d+'))[1] as INTEGER)&quot;
          excluded_sides = ['b', 'c', 'd', 'e']
          return magic_sets_cards.objects.exclude(side__in=excluded_sides).extra(select={'int': sql_number_to_int}).order_by('-set_id__release_date', 'set_id__name', 'int', 'number').all()
  
      @staticmethod
      def resolve_magic_tokens(self, info, **kwargs):
          sql_number_to_int = &quot;CAST((REGEXP_MATCH(number, '\d+'))[1] as INTEGER)&quot;
          excluded_sides = ['b', 'c', 'd', 'e']
          return magic_sets_tokens.objects.exclude(side__in=excluded_sides).extra(select={'int': sql_number_to_int}).order_by('-set_id__release_date', 'set_id__name', 'int', 'number').all()
  
  
  searchSchema = graphene.Schema(query=SearchQuery)
  </code></pre>
  <p><strong>Query</strong>:</p>
  <pre><code>{
    allProducts(name_Icontains: &quot;Spellbook&quot;, first: 12, after: &quot;&quot;) {
      pageCursors {
        previous {
          cursor
        }
        first {
          cursor
          page
        }
        around {
          cursor
          isCurrent
          page
        }
        last {
          cursor
          page
        }
        next {
          cursor
        }
      }
      edges {
        node {
          ... on MagicCards {
            name
          }
          ... on MagicTokens {
            name
          }
        }
      }
    }
  }
  </code></pre>
  <p>Now I could have the following Query, however it would mean that each product type would be under its own nest in the Json response with its own page cursors which I am not looking for.</p>
  <pre><code>{
    magicCards(name_Icontains: &quot;Spellbook&quot;, first: 12, after: &quot;&quot;) {
      pageCursors {
        ...
      }
      edges {
        node {
          name
        }
      }
    }
    magicTokens(name_Icontains: &quot;Spellbook&quot;, first: 12, after: &quot;&quot;) {
      pageCursors {
        ...
      }
      edges {
        node {
          name
        }
      }
    }
  }
  </code></pre>
  <p>EDIT: adding code for ArtsyConnection:</p>
  <p><strong>fields.py</strong></p>
  <pre class="lang-py prettyprint-override"><code>from graphene import Boolean, Field, Int, List, ObjectType, String
  from graphene.relay import Connection
  from graphene_django.filter import DjangoFilterConnectionField
  
  from .helpers import convert_connection_args_to_page_options
  from .pagination import create_page_cursors
  
  
  class PageCursor(ObjectType):
      cursor = String()
      is_current = Boolean()
      page = Int()
  
  
  class PageCursors(ObjectType):
      around = List(PageCursor)
      first = Field(PageCursor)
      last = Field(PageCursor)
      next = Field(PageCursor)
      previous = Field(PageCursor)
  
  
  class ArtsyConnection(Connection):
      class Meta:
          abstract = True
      page_cursors = Field(PageCursors)
  
  
  class ArtsyConnectionField(DjangoFilterConnectionField):
      @classmethod
      def resolve_connection(cls, _connection, args, iterable, max_limit=None):
          connection = super(ArtsyConnectionField, cls).resolve_connection(
              _connection, args, iterable, max_limit
          )
          page_options = convert_connection_args_to_page_options(args)
          page_cursors = create_page_cursors(page_options, connection.length)
          connection.page_cursors = page_cursors
          return connection
  </code></pre>
  <p><strong>helpers.py</strong></p>
  <pre class="lang-py prettyprint-override"><code>from graphql_relay import from_global_id
  
  
  def convert_connection_args_to_page_options(connection_args):
      paging_params = get_paging_parameters(connection_args)
      size = paging_params.get(&quot;limit&quot;)
      offset = paging_params.get(&quot;offset&quot;)
      page = round((size + offset) / size) if size else 1
      return {&quot;page&quot;: page, &quot;size&quot;: size}
  
  
  def get_paging_parameters(args):
      [is_forward_paging, is_backward_paging] = check_paging_sanity(args)
      first = args.get(&quot;first&quot;)
      last = args.get(&quot;last&quot;)
      after = args.get(&quot;after&quot;)
      before = args.get(&quot;before&quot;)
  
      def get_id(cursor):
          _, _id = from_global_id(cursor)
          return int(_id)
  
      def next_id(cursor):
          return get_id(cursor) + 1
  
      if is_forward_paging:
          return {&quot;limit&quot;: first, &quot;offset&quot;: next_id(after) if after else 0}
      elif is_backward_paging:
          limit = last
          offset = get_id(before) - last
  
          if offset &lt; 0:
              limit = max(last + offset, 0)
              offset = 0
  
          return {&quot;limit&quot;: limit, &quot;offset&quot;: offset}
      else:
          return {}
  
  
  def check_paging_sanity(args):
      first = args.get(&quot;first&quot;)
      last = args.get(&quot;last&quot;)
      after = args.get(&quot;after&quot;)
      before = args.get(&quot;before&quot;)
      is_forward_paging = bool(first) or bool(after)
      is_backward_paging = bool(last) or bool(before)
  
      if is_forward_paging and is_backward_paging:
          raise Exception(&quot;cursor-based pagination cannot be forwards AND backwards&quot;)
      if is_forward_paging and before or is_backward_paging and after:
          raise Exception(&quot;paging must use either first/after or last/before&quot;)
      if is_forward_paging and first &lt; 0 or is_backward_paging and last &lt; 0:
          raise Exception(&quot;paging limit must be positive&quot;)
      if last and not before:
          raise Exception(&quot;when paging backwards, a 'before' argument is required&quot;)
      return [is_forward_paging, is_backward_paging]
  </code></pre>
  <p><strong>pagination.py</strong></p>
  <pre class="lang-py prettyprint-override"><code>import math
  
  from graphql_relay import to_global_id
  
  
  PREFIX = &quot;arrayconnection&quot;
  PAGE_NUMBER_CAP = 100
  
  
  def page_to_cursor(page, size):
      return to_global_id(PREFIX, (page - 1) * size - 1)
  
  
  def page_cursors_to_array(start, end, current_page, size):
      cursors = []
      for page in range(start, end + 1):
          cursors.append(page_to_cursor_object(page, current_page, size))
      return cursors
  
  
  def page_to_cursor_object(page, current_page, size):
      return {
          &quot;cursor&quot;: page_to_cursor(page, size),
          &quot;page&quot;: page,
          &quot;is_current&quot;: current_page == page,
      }
  
  
  def compute_total_pages(total_records, size):
      return min(math.ceil(total_records / size), PAGE_NUMBER_CAP)
  
  
  def create_page_cursors(page_options, total_records, max_pages=5):
      current_page = page_options[&quot;page&quot;]
      size = page_options[&quot;size&quot;]
  
      if max_pages % 2 == 0:
          print(f&quot;Max of {max_pages} passed to page cursors, using {max_pages + 1}&quot;)
          max_pages = max_pages + 1
  
      total_pages = compute_total_pages(total_records, size)
  
      if total_pages == 0:
          page_cursors = {&quot;around&quot;: [page_to_cursor_object(1, 1, size)]}
      elif total_pages &lt;= max_pages:
          page_cursors = {
              &quot;around&quot;: page_cursors_to_array(1, total_pages, current_page, size)
          }
      elif current_page &lt;= math.floor(max_pages / 2) + 1:
          page_cursors = {
              &quot;last&quot;: page_to_cursor_object(total_pages, current_page, size),
              &quot;around&quot;: page_cursors_to_array(1, max_pages - 1, current_page, size),
          }
      elif current_page &gt;= total_pages - math.floor(max_pages / 2):
          page_cursors = {
              &quot;first&quot;: page_to_cursor_object(1, current_page, size),
              &quot;around&quot;: page_cursors_to_array(
                  total_pages - max_pages + 2, total_pages, current_page, size
              ),
          }
      else:
          offset = math.floor((max_pages - 3) / 2)
          page_cursors = {
              &quot;first&quot;: page_to_cursor_object(1, current_page, size),
              &quot;around&quot;: page_cursors_to_array(
                  current_page - offset, current_page + offset, current_page, size
              ),
              &quot;last&quot;: page_to_cursor_object(total_pages, current_page, size),
          }
  
      if current_page &gt; 1 and total_pages &gt; 1:
          page_cursors[&quot;previous&quot;] = page_to_cursor_object(
              current_page - 1, current_page, size
          )
  
      if current_page &lt; total_pages and total_pages &gt; 1:
          page_cursors[&quot;next&quot;] = page_to_cursor_object(
              current_page + 1, current_page, size
          )
  
      return page_cursors
  </code></pre>