Source code for sgqlc.types.relay

'''
GraphQL Types for `Relay <https://facebook.github.io/relay/>`_
==============================================================

Exposes ``Node`` and ``Connection``, matching `Global Object
Identification
<https://facebook.github.io/relay/graphql/objectidentification.htm>`_
and `Cursor Connections
<https://facebook.github.io/relay/graphql/connections.htm>`_, which
are widely used.

Examples
--------

>>> from sgqlc.types import Type, Field, list_of
>>> class NodeBasedInterface(Node):
...     a_int = int
...
>>> NodeBasedInterface # or repr()
interface NodeBasedInterface implements Node {
  id: ID!
  aInt: Int
}
>>> class NodeBasedType(Type, Node):
...     a_int = int
...
>>> NodeBasedType # or repr()
type NodeBasedType implements Node {
  id: ID!
  aInt: Int
}

:class:`Connection` subclasses will get ``page_info`` and
``__iadd__`` to merge 2 connections:

>>> class MyEdge(Type):
...     node = NodeBasedType
...     cursor = str
...
>>> class MyConn(Connection):
...    nodes = list_of(NodeBasedType)
...    edges = list_of(MyEdge)
...
>>> MyConn # or repr()
type MyConn {
  pageInfo: PageInfo!
  nodes: [NodeBasedType]
  edges: [MyEdge]
}
>>> class MyTypeWithConn(Type):
...     conn = Field(MyConn, args=connection_args())
...
>>> MyTypeWithConn # or repr()
type MyTypeWithConn {
  conn(
    after: String
    before: String
    first: Int
    last: Int
  ): MyConn
}

Given ``json_data1`` being the contents of the GraphQL query::

   query {
      getMyTypeWithConn(id: "...") {
        conn(first: 2) { # first page
          pageInfo { startCursor, endCursor, hasNextPage, hasPreviousPage }
          nodes { id, aInt }
          edges { cursor, node { id, aInt } }
        }
      }
   }

>>> json_data1 = { # page 1 (2 elements of 4)
...     'pageInfo': {
...         'startCursor': 'cursor-1',
...         'endCursor': 'cursor-2',
...         'hasNextPage': True,
...         'hasPreviousPage': False,
...     },
...     'nodes': [
...         {'id': '1111', 'aInt': 1},
...         {'id': '2222', 'aInt': 2},
...     ],
...     'edges': [
...         {'cursor': 'cursor-1', 'node': {'id': '1111', 'aInt': 1}},
...         {'cursor': 'cursor-2', 'node': {'id': '2222', 'aInt': 2}},
...     ],
... }
>>> conn1 = MyConn(json_data1)
>>> print(conn1.page_info)  # doctest: +ELLIPSIS
PageInfo(end_cursor=cursor-2, start_cursor=cursor-1, has_next_page=True...
>>> for n in conn1.nodes:
...     print(repr(n))
NodeBasedType(id='1111', a_int=1)
NodeBasedType(id='2222', a_int=2)
>>> for e in conn1.edges:
...     print(repr(e))
MyEdge(node=NodeBasedType(id='1111', a_int=1), cursor='cursor-1')
MyEdge(node=NodeBasedType(id='2222', a_int=2), cursor='cursor-2')


We'd execute the query to fetch the second page as ``json_data2``::

   query {
      getMyTypeWithConn(id: "...") {
        conn(first: 2, after: "cursor-2") { # second page
          pageInfo { startCursor, endCursor, hasNextPage, hasPreviousPage }
          nodes { id, aInt }
          edges { cursor, node { id, aInt } }
        }
      }
   }

>>> json_data2 = { # page 2 (2 elements of 4)
...     'pageInfo': {
...         'startCursor': 'cursor-3',
...         'endCursor': 'cursor-4',
...         'hasNextPage': False,
...         'hasPreviousPage': True,
...     },
...     'nodes': [
...         {'id': '3333', 'aInt': 3},
...         {'id': '4444', 'aInt': 4},
...     ],
...     'edges': [
...         {'cursor': 'cursor-3', 'node': {'id': '3333', 'aInt': 3}},
...         {'cursor': 'cursor-4', 'node': {'id': '4444', 'aInt': 4}},
...     ],
... }
>>> conn2 = MyConn(json_data2)
>>> print(conn2.page_info)  # doctest: +ELLIPSIS
PageInfo(end_cursor=cursor-4, start_cursor=cursor-3, has_next_page=False...
>>> for n in conn2.nodes:
...     print(repr(n))
NodeBasedType(id='3333', a_int=3)
NodeBasedType(id='4444', a_int=4)
>>> for e in conn2.edges:
...     print(repr(e))
MyEdge(node=NodeBasedType(id='3333', a_int=3), cursor='cursor-3')
MyEdge(node=NodeBasedType(id='4444', a_int=4), cursor='cursor-4')

One can merge ``conn2`` into ``conn1``, also updating its backing
store ``json_data1``:

>>> conn1 += conn2
>>> print(conn1.page_info)  # doctest: +ELLIPSIS
PageInfo(end_cursor=cursor-4, start_cursor=cursor-1, has_next_page=False...
>>> for n in conn1.nodes:
...     print(repr(n))
NodeBasedType(id='1111', a_int=1)
NodeBasedType(id='2222', a_int=2)
NodeBasedType(id='3333', a_int=3)
NodeBasedType(id='4444', a_int=4)
>>> for e in conn1.edges:
...     print(repr(e))
MyEdge(node=NodeBasedType(id='1111', a_int=1), cursor='cursor-1')
MyEdge(node=NodeBasedType(id='2222', a_int=2), cursor='cursor-2')
MyEdge(node=NodeBasedType(id='3333', a_int=3), cursor='cursor-3')
MyEdge(node=NodeBasedType(id='4444', a_int=4), cursor='cursor-4')
>>> import json
>>> print(json.dumps(json_data1, sort_keys=True, indent=2))
{
  "edges": [
    {
      "cursor": "cursor-1",
      "node": {
        "aInt": 1,
        "id": "1111"
      }
    },
    {
      "cursor": "cursor-2",
      "node": {
        "aInt": 2,
        "id": "2222"
      }
    },
    {
      "cursor": "cursor-3",
      "node": {
        "aInt": 3,
        "id": "3333"
      }
    },
    {
      "cursor": "cursor-4",
      "node": {
        "aInt": 4,
        "id": "4444"
      }
    }
  ],
  "nodes": [
    {
      "aInt": 1,
      "id": "1111"
    },
    {
      "aInt": 2,
      "id": "2222"
    },
    {
      "aInt": 3,
      "id": "3333"
    },
    {
      "aInt": 4,
      "id": "4444"
    }
  ],
  "pageInfo": {
    "endCursor": "cursor-4",
    "hasNextPage": false,
    "hasPreviousPage": false,
    "startCursor": "cursor-1"
  }
}

When merging, the receiver connection can be empty:

>>> json_data0 = {}
>>> conn0 = MyConn(json_data0)
>>> conn0 += conn1
>>> print(conn0.page_info)  # doctest: +ELLIPSIS
PageInfo(end_cursor=cursor-4, start_cursor=cursor-1, has_next_page=False...
>>> for n in conn0.nodes:
...     print(repr(n))
NodeBasedType(id='1111', a_int=1)
NodeBasedType(id='2222', a_int=2)
NodeBasedType(id='3333', a_int=3)
NodeBasedType(id='4444', a_int=4)
>>> for e in conn0.edges:
...     print(repr(e))
MyEdge(node=NodeBasedType(id='1111', a_int=1), cursor='cursor-1')
MyEdge(node=NodeBasedType(id='2222', a_int=2), cursor='cursor-2')
MyEdge(node=NodeBasedType(id='3333', a_int=3), cursor='cursor-3')
MyEdge(node=NodeBasedType(id='4444', a_int=4), cursor='cursor-4')
>>> print(json.dumps(json_data0, sort_keys=True, indent=2))
{
  "edges": [
    {
      "cursor": "cursor-1",
      "node": {
        "aInt": 1,
        "id": "1111"
      }
    },
    {
      "cursor": "cursor-2",
      "node": {
        "aInt": 2,
        "id": "2222"
      }
    },
    {
      "cursor": "cursor-3",
      "node": {
        "aInt": 3,
        "id": "3333"
      }
    },
    {
      "cursor": "cursor-4",
      "node": {
        "aInt": 4,
        "id": "4444"
      }
    }
  ],
  "nodes": [
    {
      "aInt": 1,
      "id": "1111"
    },
    {
      "aInt": 2,
      "id": "2222"
    },
    {
      "aInt": 3,
      "id": "3333"
    },
    {
      "aInt": 4,
      "id": "4444"
    }
  ],
  "pageInfo": {
    "endCursor": "cursor-4",
    "hasNextPage": false,
    "hasPreviousPage": false,
    "startCursor": "cursor-1"
  }
}



:license: ISC
'''

__docformat__ = 'reStructuredText en'

__all__ = ('Node', 'PageInfo', 'Connection', 'connection_args')

from . import Type, Interface, non_null, ArgDict, String, Int


[docs]class Node(Interface): '''Global Object Identification based on Relay specification. https://facebook.github.io/relay/graphql/objectidentification.htm ''' id = non_null(id) # noqa: A003
[docs]class PageInfo(Type): ''':class:`Connection` page information. https://facebook.github.io/relay/graphql/connections.htm ''' end_cursor = str start_cursor = str has_next_page = non_null(bool) has_previous_page = non_null(bool)
[docs]class Connection(Type): '''Cursor Connections based on Relay specification. https://facebook.github.io/relay/graphql/connections.htm .. note:: This class exposes ``+=`` (in-place addition) operator to append information from another connection into this. The usage is as follow, if ``obj.connection.page_info.has_next_page``, then you should query the next page using ``after=obj.connection.page_info.end_cursor``. The resulting object should be ``obj.connection += obj2.connection``, this will add the contents of ``obj2.connection`` to ``obj.connection``, resetting ``obj.connection.page_info.has_next_page``, ``obj.connection.page_info.end_cursor`` and the JSON backing store, if any. ''' __auto_register = False # do not expose this in Schema, just subclasses page_info = non_null(PageInfo) def __iadd__(self, other): # NOTE: assign to list, not '+=', so ContainerType.__setattr__() # is called to apply to backing store has_self_nodes = hasattr(self, 'nodes') and self.nodes is not None has_other_nodes = hasattr(other, 'nodes') and other.nodes is not None if has_self_nodes and has_other_nodes: self.nodes = self.nodes + other.nodes elif has_other_nodes: self.nodes = other.nodes has_self_edges = hasattr(self, 'edges') and self.edges is not None has_other_edges = hasattr(other, 'edges') and other.edges is not None if has_self_edges and has_other_edges: self.edges = self.edges + other.edges elif has_other_edges: self.edges = other.edges has_self_page_info = ( hasattr(self, 'page_info') and self.page_info is not None ) has_other_page_info = ( hasattr(other, 'page_info') and other.page_info is not None ) if has_self_page_info and has_other_page_info: if hasattr(other.page_info, 'end_cursor'): self.page_info.end_cursor = other.page_info.end_cursor self.page_info.has_next_page = other.page_info.has_next_page elif has_other_page_info: self.page_info = other.page_info return self
[docs]def connection_args(*lst, **mapping): '''Returns the default parameters for connection. Extra parameters may be given as argument, both as iterable, positional tuples or mapping. By default, provides: - ``after: String`` - ``before: String`` - ``first: Int`` - ``last: Int`` ''' pd = ArgDict(*lst, **mapping) pd.setdefault('after', String) pd.setdefault('before', String) pd.setdefault('first', Int) pd.setdefault('last', Int) return pd