pax_global_header00006660000000000000000000000064135725526720014530gustar00rootroot0000000000000052 comment=54fa42716e587c21e1fd8e6fc7d39cb32db28039 graphql-relay-py-2.0.1/000077500000000000000000000000001357255267200147265ustar00rootroot00000000000000graphql-relay-py-2.0.1/.flake8000066400000000000000000000002061357255267200160770ustar00rootroot00000000000000[flake8] ignore = E203,W503,E704 exclude = .git,.mypy_cache,.pytest_cache,.tox,.venv,__pycache__,build,dist,docs max-line-length = 88 graphql-relay-py-2.0.1/.gitignore000066400000000000000000000007601357255267200167210ustar00rootroot00000000000000__pycache__/ *.py[cod] .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST *.manifest *.spec pip-log.txt pip-delete-this-directory.txt htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .pytest_cache/ *.mo *.pot *.log docs/_build/ target/ .python-version .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ .mypy_cache/ .idea/ graphql-relay-py-2.0.1/.travis.yml000066400000000000000000000022121357255267200170340ustar00rootroot00000000000000language: python dist: xenial python: - "2.7" - "3.5" - "3.6" - "3.7" - "pypy3" install: - pip install . - pip install "flake8>=3.7,<4" script: - python setup.py test -a "--cov=graphql_relay" - flake8 setup.py src tests after_success: - pip install coveralls - coveralls deploy: provider: pypi distributions: sdist bdist_wheel on: branch: master tags: true python: 3.7 skip_existing: true user: mvanlonden password: secure: gu79sV5e3wkUI8eH20TbBRctOOGkuWe6EQUjJP17q1Jm15/sJsehOaaLPiG0psxtqBGABp+z+xXkjhRfNMowY2xJPLjGmpF23pOIWGjBWKLKXzJStegKM6kwJYyHzcykIhBEb74047FhqhcwOgZcsp6sZpfzuby+onevLzPDI0C4jTARw7WAqMln9JIBZJyoQKp9fpr8AoXL2JtHr6sjkeJ94lz1XViL9rtTo7qFYfqYS3XdKO0wJhLfMUXSihIzSWQykBVv+75tMKQr0CtGALXcJSRGT6Sb31GiFyG93rtcOcBvbjFt1MK8aNKyIMhwgSqjcgKvxWAvXn4MsCaabHPVEv0YuT9t9cQzaAvi81LqHkpALgpDfXFfsMzHG18/8ME9TpM8u52r/ST5lhukglfnxWVy4hg8VLZ0iiTtpS3hx1Ba4Uecr++6fI5X+KL3EPabApQM+t4rcC0h4mEbjq3IkZ/ANAIJ2UjKTMcUkbQbKqJ1MY4xQyw+vVugffBmhEWSb4GnQPEadMGD6qfUI+t7epDP0ipp67rOiUooFYGabQp40pf7MxFPG23fvJ3JWbo3fzcIRmQiSalrIL1gFXH2DQnv4xhZvwnci+dIK29mamH0CfmjAJ8BdfzRsjV156BZbfkzXRfWDMes9G2E8S27xJJ2N4ySNIMkaXsu1u4= graphql-relay-py-2.0.1/LICENSE000066400000000000000000000020671357255267200157400ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Syrus Akbary Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. graphql-relay-py-2.0.1/MANIFEST.in000066400000000000000000000002431357255267200164630ustar00rootroot00000000000000include MANIFEST.in include LICENSE include README.md include .flake8 include tox.ini graft src/graphql_relay graft tests global-exclude *.py[co] __pycache__ graphql-relay-py-2.0.1/README.md000066400000000000000000000214031357255267200162050ustar00rootroot00000000000000# Relay Library for GraphQL Python This is a library to allow the easy creation of Relay-compliant servers using the [GraphQL Python](https://github.com/graphql-python/graphql-core) reference implementation of a GraphQL server. *Note: The code is a __exact__ port of the original [graphql-relay js implementation](https://github.com/graphql/graphql-relay-js) from Facebook* [![PyPI version](https://badge.fury.io/py/graphql-relay.svg)](https://badge.fury.io/py/graphql-relay) [![Build Status](https://travis-ci.org/graphql-python/graphql-relay-py.svg?branch=master)](https://travis-ci.org/graphql-python/graphql-relay-py) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphql-relay-py/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphql-relay-py?branch=master) ## Getting Started A basic understanding of GraphQL and of the GraphQL Python implementation is needed to provide context for this library. An overview of GraphQL in general is available in the [README](https://github.com/graphql-python/graphql-core/blob/master/README.md) for the [Specification for GraphQL](https://github.com/graphql-python/graphql-core). This library is designed to work with the the [GraphQL Python](https://github.com/graphql-python/graphql-core) reference implementation of a GraphQL server. An overview of the functionality that a Relay-compliant GraphQL server should provide is in the [GraphQL Relay Specification](https://facebook.github.io/relay/docs/graphql-relay-specification.html) on the [Relay website](https://facebook.github.io/relay/). That overview describes a simple set of examples that exist as [tests](tests) in this repository. A good way to get started with this repository is to walk through that documentation and the corresponding tests in this library together. ## Using Relay Library for GraphQL Python (graphql-core) Install Relay Library for GraphQL Python ```sh pip install "graphql-core>=2,<3" # use version 2.x of graphql-core pip install graphql-relay ``` When building a schema for [GraphQL](https://github.com/graphql-python/graphql-core), the provided library functions can be used to simplify the creation of Relay patterns. ### Connections Helper functions are provided for both building the GraphQL types for connections and for implementing the `resolver` method for fields returning those types. - `connection_args` returns the arguments that fields should provide when they return a connection type. - `connection_definitions` returns a `connection_type` and its associated `edgeType`, given a name and a node type. - `connection_from_list` is a helper method that takes a list and the arguments from `connection_args`, does pagination and filtering, and returns an object in the shape expected by a `connection_type`'s `resolver` function. - `connection_from_promised_list` is similar to `connection_from_list`, but it takes a promise that resolves to an array, and returns a promise that resolves to the expected shape by `connection_type`. - `cursor_for_object_in_connection` is a helper method that takes a list and a member object, and returns a cursor for use in the mutation payload. An example usage of these methods from the [test schema](tests/starwars/schema.py): ```python ship_edge, ship_connection = connection_definitions('Ship', shipType) factionType = GraphQLObjectType( name='Faction', description='A faction in the Star Wars saga', fields= lambda: { 'id': global_id_field('Faction'), 'name': GraphQLField( GraphQLString, description='The name of the faction.', ), 'ships': GraphQLField( ship_connection, description='The ships used by the faction.', args=connection_args, resolver=lambda faction, _info, **args: connection_from_list( [getShip(ship) for ship in faction.ships], args ), ) }, interfaces=[node_interface] ) ``` This shows adding a `ships` field to the `Faction` object that is a connection. It uses `connection_definitions({name: 'Ship', nodeType: shipType})` to create the connection type, adds `connection_args` as arguments on this function, and then implements the resolver function by passing the list of ships and the arguments to `connection_from_list`. ### Object Identification Helper functions are provided for both building the GraphQL types for nodes and for implementing global IDs around local IDs. - `node_definitions` returns the `Node` interface that objects can implement, and returns the `node` root field to include on the query type. To implement this, it takes a function to resolve an ID to an object, and to determine the type of a given object. - `to_global_id` takes a type name and an ID specific to that type name, and returns a "global ID" that is unique among all types. - `from_global_id` takes the "global ID" created by `to_global_id`, and returns the type name and ID used to create it. - `global_id_field` creates the configuration for an `id` field on a node. - `plural_identifying_root_field` creates a field that accepts a list of non-ID identifiers (like a username) and maps then to their corresponding objects. An example usage of these methods from the [test schema](tests/starwars/schema.py): ```python def get_node(global_id, _info): type_, id_ = from_global_id(global_id) if type_ == 'Faction': return getFaction(id_) elif type_ == 'Ship': return getShip(id_) else: return None def get_node_type(obj, _info): if isinstance(obj, Faction): return factionType else: return shipType node_interface, node_field = node_definitions(get_node, get_node_type) factionType = GraphQLObjectType( name= 'Faction', description= 'A faction in the Star Wars saga', fields= lambda: { 'id': global_id_field('Faction'), }, interfaces= [node_interface] ) queryType = GraphQLObjectType( name= 'Query', fields= lambda: { 'node': node_field } ) ``` This uses `node_definitions` to construct the `Node` interface and the `node` field; it uses `from_global_id` to resolve the IDs passed in in the implementation of the function mapping ID to object. It then uses the `global_id_field` method to create the `id` field on `Faction`, which also ensures implements the `node_interface`. Finally, it adds the `node` field to the query type, using the `node_field` returned by `node_definitions`. ### Mutations A helper function is provided for building mutations with single inputs and client mutation IDs. - `mutation_with_client_mutation_id` takes a name, input fields, output fields, and a mutation method to map from the input fields to the output fields, performing the mutation along the way. It then creates and returns a field configuration that can be used as a top-level field on the mutation type. An example usage of these methods from the [test schema](tests/starwars/schema.py): ```python class IntroduceShipMutation(object): def __init__(self, shipId, factionId, clientMutationId=None): self.shipId = shipId self.factionId = factionId self.clientMutationId = clientMutationId def mutate_and_get_payload(_info, shipName, factionId, **_input): newShip = createShip(shipName, factionId) return IntroduceShipMutation( shipId=newShip.id, factionId=factionId, ) shipMutation = mutation_with_client_mutation_id( 'IntroduceShip', input_fields={ 'shipName': GraphQLField( GraphQLNonNull(GraphQLString) ), 'factionId': GraphQLField( GraphQLNonNull(GraphQLID) ) }, output_fields= { 'ship': GraphQLField( shipType, resolver=lambda payload, _info: getShip(payload.shipId) ), 'faction': GraphQLField( factionType, resolver=lambda payload, _info: getFaction(payload.factionId) ) }, mutate_and_get_payload=mutate_and_get_payload ) mutationType = GraphQLObjectType( 'Mutation', fields=lambda: { 'introduceShip': shipMutation } ) ``` This code creates a mutation named `IntroduceShip`, which takes a faction ID and a ship name as input. It outputs the `Faction` and the `Ship` in question. `mutate_and_get_payload` then gets an object with a property for each input field, performs the mutation by constructing the new ship, then returns an object that will be resolved by the output fields. Our mutation type then creates the `introduceShip` field using the return value of `mutation_with_client_mutation_id`. ## Contributing After cloning this repo, ensure dependencies are installed by running: ```sh python setup.py install ``` After developing, the full test suite can be evaluated by running: ```sh python setup.py test # Use --pytest-args="-v -s" for verbose mode ``` graphql-relay-py-2.0.1/graphql_relay/000077500000000000000000000000001357255267200175605ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/__init__.py000066400000000000000000000017211357255267200216720ustar00rootroot00000000000000from .connection.connection import ( connection_args, connection_definitions ) from .connection.arrayconnection import ( connection_from_list, connection_from_promised_list, cursor_for_object_in_connection ) from .node.node import ( node_definitions, from_global_id, to_global_id, global_id_field, ) from .mutation.mutation import ( mutation_with_client_mutation_id ) __all__ = [ # Helpers for creating connection types in the schema 'connection_args', 'connection_definitions', # Helpers for creating connections from arrays 'connection_from_list', 'connection_from_promised_list', 'cursor_for_object_in_connection', # Helper for creating node definitions 'node_definitions', # Utilities for creating global IDs in systems that don't have them 'from_global_id', 'to_global_id', 'global_id_field', # Helper for creating mutations with client mutation IDs 'mutation_with_client_mutation_id' ] graphql-relay-py-2.0.1/graphql_relay/connection/000077500000000000000000000000001357255267200217175ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/connection/__init__.py000066400000000000000000000000001357255267200240160ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/connection/arrayconnection.py000066400000000000000000000110411357255267200254640ustar00rootroot00000000000000from ..utils import base64, unbase64, is_str from .connectiontypes import Connection, PageInfo, Edge def connection_from_list(data, args=None, **kwargs): """ A simple function that accepts an array and connection arguments, and returns a connection object for use in GraphQL. It uses array offsets as pagination, so pagination will only work if the array is static. """ _len = len(data) return connection_from_list_slice( data, args, slice_start=0, list_length=_len, list_slice_length=_len, **kwargs ) def connection_from_promised_list(data_promise, args=None, **kwargs): """ A version of `connectionFromArray` that takes a promised array, and returns a promised connection. """ return data_promise.then(lambda data: connection_from_list(data, args, **kwargs)) def connection_from_list_slice(list_slice, args=None, connection_type=None, edge_type=None, pageinfo_type=None, slice_start=0, list_length=0, list_slice_length=None): """ Given a slice (subset) of an array, returns a connection object for use in GraphQL. This function is similar to `connectionFromArray`, but is intended for use cases where you know the cardinality of the connection, consider it too large to materialize the entire array, and instead wish pass in a slice of the total result large enough to cover the range specified in `args`. """ connection_type = connection_type or Connection edge_type = edge_type or Edge pageinfo_type = pageinfo_type or PageInfo args = args or {} before = args.get('before') after = args.get('after') first = args.get('first') last = args.get('last') if list_slice_length is None: list_slice_length = len(list_slice) slice_end = slice_start + list_slice_length before_offset = get_offset_with_default(before, list_length) after_offset = get_offset_with_default(after, -1) start_offset = max( slice_start - 1, after_offset, -1 ) + 1 end_offset = min( slice_end, before_offset, list_length ) if isinstance(first, int): end_offset = min( end_offset, start_offset + first ) if isinstance(last, int): start_offset = max( start_offset, end_offset - last ) # If supplied slice is too large, trim it down before mapping over it. _slice = list_slice[ max(start_offset - slice_start, 0): list_slice_length - (slice_end - end_offset) ] edges = [ edge_type( node=node, cursor=offset_to_cursor(start_offset + i) ) for i, node in enumerate(_slice) ] first_edge_cursor = edges[0].cursor if edges else None last_edge_cursor = edges[-1].cursor if edges else None lower_bound = after_offset + 1 if after else 0 upper_bound = before_offset if before else list_length return connection_type( edges=edges, page_info=pageinfo_type( start_cursor=first_edge_cursor, end_cursor=last_edge_cursor, has_previous_page=isinstance(last, int) and start_offset > lower_bound, has_next_page=isinstance(first, int) and end_offset < upper_bound ) ) PREFIX = 'arrayconnection:' def connection_from_promised_list_slice(data_promise, args=None, **kwargs): return data_promise.then( lambda data: connection_from_list_slice(data, args, **kwargs)) def offset_to_cursor(offset): """ Creates the cursor string from an offset. """ return base64(PREFIX + str(offset)) def cursor_to_offset(cursor): """ Rederives the offset from the cursor string. """ try: return int(unbase64(cursor)[len(PREFIX):]) except Exception: return None def cursor_for_object_in_connection(data, _object): """ Return the cursor associated with an object in an array. """ if _object not in data: return None offset = data.index(_object) return offset_to_cursor(offset) def get_offset_with_default(cursor=None, default_offset=0): """ Given an optional cursor and a default offset, returns the offset to use; if the cursor contains a valid offset, that will be used, otherwise it will be the default. """ if not is_str(cursor): return default_offset offset = cursor_to_offset(cursor) try: return int(offset) except Exception: return default_offset graphql-relay-py-2.0.1/graphql_relay/connection/connection.py000066400000000000000000000052721357255267200244360ustar00rootroot00000000000000from collections import OrderedDict from graphql.type import ( GraphQLArgument, GraphQLBoolean, GraphQLInt, GraphQLNonNull, GraphQLList, GraphQLObjectType, GraphQLString, GraphQLField ) from ..utils import resolve_maybe_thunk connection_args = OrderedDict(( ('before', GraphQLArgument(GraphQLString)), ('after', GraphQLArgument(GraphQLString)), ('first', GraphQLArgument(GraphQLInt)), ('last', GraphQLArgument(GraphQLInt)), )) def connection_definitions( name, node_type, resolve_node=None, resolve_cursor=None, edge_fields=None, connection_fields=None): edge_fields = edge_fields or OrderedDict() connection_fields = connection_fields or OrderedDict() edge_type = GraphQLObjectType( name + 'Edge', description='An edge in a connection.', fields=lambda: OrderedDict(( ('node', GraphQLField( node_type, resolver=resolve_node, description='The item at the end of the edge', )), ('cursor', GraphQLField( GraphQLNonNull(GraphQLString), resolver=resolve_cursor, description='A cursor for use in pagination', )), ), **resolve_maybe_thunk(edge_fields)) ) connection_type = GraphQLObjectType( name + 'Connection', description='A connection to a list of items.', fields=lambda: OrderedDict(( ('pageInfo', GraphQLField( GraphQLNonNull(page_info_type), description='The Information to aid in pagination', )), ('edges', GraphQLField( GraphQLList(edge_type), description='A list of edges.', )), ), **resolve_maybe_thunk(connection_fields)) ) return edge_type, connection_type # The common page info type used by all connections. page_info_type = GraphQLObjectType( 'PageInfo', description='Information about pagination in a connection.', fields=lambda: OrderedDict(( ('hasNextPage', GraphQLField( GraphQLNonNull(GraphQLBoolean), description='When paginating forwards, are there more items?', )), ('hasPreviousPage', GraphQLField( GraphQLNonNull(GraphQLBoolean), description='When paginating backwards, are there more items?', )), ('startCursor', GraphQLField( GraphQLString, description='When paginating backwards, the cursor to continue.', )), ('endCursor', GraphQLField( GraphQLString, description='When paginating forwards, the cursor to continue.', )), )) ) graphql-relay-py-2.0.1/graphql_relay/connection/connectiontypes.py000066400000000000000000000020531357255267200255150ustar00rootroot00000000000000class Connection(object): def __init__(self, edges, page_info): self.edges = edges self.page_info = page_info def to_dict(self): return { 'edges': [e.to_dict() for e in self.edges], 'pageInfo': self.page_info.to_dict(), } class PageInfo(object): def __init__(self, start_cursor="", end_cursor="", has_previous_page=False, has_next_page=False): self.startCursor = start_cursor self.endCursor = end_cursor self.hasPreviousPage = has_previous_page self.hasNextPage = has_next_page def to_dict(self): return { 'startCursor': self.startCursor, 'endCursor': self.endCursor, 'hasPreviousPage': self.hasPreviousPage, 'hasNextPage': self.hasNextPage, } class Edge(object): def __init__(self, node, cursor): self.node = node self.cursor = cursor def to_dict(self): return { 'node': self.node, 'cursor': self.cursor, } graphql-relay-py-2.0.1/graphql_relay/connection/tests/000077500000000000000000000000001357255267200230615ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/connection/tests/__init__.py000066400000000000000000000000001357255267200251600ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/connection/tests/test_arrayconnection.py000066400000000000000000000542661357255267200277050ustar00rootroot00000000000000from promise import Promise from ..arrayconnection import ( connection_from_list, connection_from_list_slice, connection_from_promised_list, connection_from_promised_list_slice, cursor_for_object_in_connection ) letters = ['A', 'B', 'C', 'D', 'E'] letters_promise = Promise.resolve(letters) def test_returns_all_elements_without_filters(): c = connection_from_list(letters) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_respects_a_smaller_first(): c = connection_from_list(letters, dict(first=2)) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_respects_an_overly_large_first(): c = connection_from_list(letters, dict(first=10)) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_respects_a_smaller_last(): c = connection_from_list(letters, dict(last=2)) expected = { 'edges': [ { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': True, 'hasNextPage': False, } } assert c.to_dict() == expected def test_respects_an_overly_large_last(): c = connection_from_list(letters, dict(last=10)) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_pagination_respects_first_after(): c = connection_from_list(letters, dict(first=2, after='YXJyYXljb25uZWN0aW9uOjE=')) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_pagination_respects_longfirst_after(): c = connection_from_list( letters, dict(first=10, after='YXJyYXljb25uZWN0aW9uOjE=')) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_pagination_respects_last_before(): c = connection_from_list(letters, dict(last=2, before='YXJyYXljb25uZWN0aW9uOjM=')) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': True, 'hasNextPage': False, } } assert c.to_dict() == expected def test_pagination_respects_longlast_before(): c = connection_from_list( letters, dict(last=10, before='YXJyYXljb25uZWN0aW9uOjM=')) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_first_after_before_few(): c = connection_from_list(letters, dict( first=2, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_first_after_before_many(): c = connection_from_list(letters, dict( first=4, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_first_after_before_exact(): c = connection_from_list(letters, dict( first=3, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_last_after_before_few(): c = connection_from_list(letters, dict( last=2, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': True, 'hasNextPage': False, } } assert c.to_dict() == expected def test_last_after_before_many(): c = connection_from_list(letters, dict( last=4, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_last_after_before_exact(): c = connection_from_list(letters, dict( last=3, after='YXJyYXljb25uZWN0aW9uOjA=', before='YXJyYXljb25uZWN0aW9uOjQ=', )) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_no_elements_first_0(): c = connection_from_list(letters, dict(first=0)) expected = { 'edges': [ ], 'pageInfo': { 'startCursor': None, 'endCursor': None, 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_all_elements_invalid_cursors(): c = connection_from_list(letters, dict(before='invalid', after='invalid')) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_all_elements_cursor_outside(): c = connection_from_list(letters, dict( before='YXJyYXljb25uZWN0aW9uOjYK', after='YXJyYXljb25uZWN0aW9uOi0xCg==' )) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_no_elements_cursors_cross(): c = connection_from_list(letters, dict( before='YXJyYXljb25uZWN0aW9uOjI=', after='YXJyYXljb25uZWN0aW9uOjQ=' )) expected = { 'edges': [ ], 'pageInfo': { 'startCursor': None, 'endCursor': None, 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_cursor_for_object_in_connection_member_object(): letter_b_cursor = cursor_for_object_in_connection(letters, 'B') assert letter_b_cursor == 'YXJyYXljb25uZWN0aW9uOjE=' def test_cursor_for_object_in_connection_non_member_object(): letter_b_cursor = cursor_for_object_in_connection(letters, 'F') assert letter_b_cursor is None def test_promised_list_returns_all_elements_without_filters(): c = connection_from_promised_list(letters_promise) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.value.to_dict() == expected def test_promised_list_respects_a_smaller_first(): c = connection_from_promised_list(letters_promise, dict(first=2)) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.value.to_dict() == expected def test_list_slice_works_with_a_just_right_array_slice(): c = connection_from_list_slice( letters[1:3], dict( first=2, after='YXJyYXljb25uZWN0aW9uOjA=', ), slice_start=1, list_length=5 ) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_list_slice_works_with_an_oversized_array_slice_left_side(): c = connection_from_list_slice( letters[0:3], dict( first=2, after='YXJyYXljb25uZWN0aW9uOjA=', ), slice_start=0, list_length=5 ) expected = { 'edges': [ { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_list_slice_works_with_an_oversized_array_slice_right_side(): c = connection_from_list_slice( letters[2:4], dict( first=1, after='YXJyYXljb25uZWN0aW9uOjE=', ), slice_start=2, list_length=5 ) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_list_slice_works_with_an_oversized_array_slice_both_sides(): c = connection_from_list_slice( letters[1:4], dict( first=1, after='YXJyYXljb25uZWN0aW9uOjE=', ), slice_start=1, list_length=5 ) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_list_slice_works_with_an_undersized_array_slice_left_side(): c = connection_from_list_slice( letters[3:5], dict( first=3, after='YXJyYXljb25uZWN0aW9uOjE=', ), slice_start=3, list_length=5 ) expected = { 'edges': [ { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, { 'node': 'E', 'cursor': 'YXJyYXljb25uZWN0aW9uOjQ=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjQ=', 'hasPreviousPage': False, 'hasNextPage': False, } } assert c.to_dict() == expected def test_list_slice_works_with_an_undersized_array_slice_right_side(): c = connection_from_list_slice( letters[2:4], dict( first=3, after='YXJyYXljb25uZWN0aW9uOjE=', ), slice_start=2, list_length=5 ) expected = { 'edges': [ { 'node': 'C', 'cursor': 'YXJyYXljb25uZWN0aW9uOjI=', }, { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjI=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_list_slice_works_with_an_undersized_array_slice_both_sides(): c = connection_from_list_slice( letters[3:4], dict( first=3, after='YXJyYXljb25uZWN0aW9uOjE=', ), slice_start=3, list_length=5 ) expected = { 'edges': [ { 'node': 'D', 'cursor': 'YXJyYXljb25uZWN0aW9uOjM=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjM=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.to_dict() == expected def test_promised_list_slice_respects_a_smaller_first(): letters_promise_slice = Promise.resolve(letters[:3]) c = connection_from_promised_list_slice( letters_promise_slice, dict(first=2), slice_start=0, list_length=5 ) expected = { 'edges': [ { 'node': 'A', 'cursor': 'YXJyYXljb25uZWN0aW9uOjA=', }, { 'node': 'B', 'cursor': 'YXJyYXljb25uZWN0aW9uOjE=', }, ], 'pageInfo': { 'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=', 'endCursor': 'YXJyYXljb25uZWN0aW9uOjE=', 'hasPreviousPage': False, 'hasNextPage': True, } } assert c.value.to_dict() == expected graphql-relay-py-2.0.1/graphql_relay/connection/tests/test_connection.py000066400000000000000000000102621357255267200266320ustar00rootroot00000000000000from collections import namedtuple from graphql import graphql from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLInt, GraphQLString, ) from ..arrayconnection import connection_from_list from ..connection import ( connection_args, connection_definitions ) User = namedtuple('User', ['name', 'friends']) allUsers = [ User(name='Dan', friends=[1, 2, 3, 4]), User(name='Nick', friends=[0, 2, 3, 4]), User(name='Lee', friends=[0, 1, 3, 4]), User(name='Joe', friends=[0, 1, 2, 4]), User(name='Tim', friends=[0, 1, 2, 3]), ] userType = GraphQLObjectType( 'User', fields=lambda: { 'name': GraphQLField(GraphQLString), 'friends': GraphQLField( friendConnection, args=connection_args, resolver=lambda user, _info, **args: connection_from_list(user.friends, args), ), }, ) friendEdge, friendConnection = connection_definitions( 'Friend', userType, resolve_node=lambda edge, _info: allUsers[edge.node], edge_fields=lambda: { 'friendshipTime': GraphQLField( GraphQLString, resolver=lambda _user, _info: 'Yesterday' ), }, connection_fields=lambda: { 'totalCount': GraphQLField( GraphQLInt, resolver=lambda _user, _info: len(allUsers) - 1 ), } ) queryType = GraphQLObjectType( 'Query', fields=lambda: { 'user': GraphQLField( userType, resolver=lambda _root, _info: allUsers[0] ), } ) schema = GraphQLSchema(query=queryType) def test_include_connections_and_edge_types(): query = ''' query FriendsQuery { user { friends(first: 2) { totalCount edges { friendshipTime node { name } } } } } ''' expected = { 'user': { 'friends': { 'totalCount': 4, 'edges': [ { 'friendshipTime': 'Yesterday', 'node': { 'name': 'Nick' } }, { 'friendshipTime': 'Yesterday', 'node': { 'name': 'Lee' } }, ] } } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_works_with_forward_connection_args(): query = ''' query FriendsQuery { user { friendsForward: friends(first: 2) { edges { node { name } } } } } ''' expected = { 'user': { 'friendsForward': { 'edges': [ { 'node': { 'name': 'Nick' } }, { 'node': { 'name': 'Lee' } }, ] } } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_works_with_backward_connection_args(): query = ''' query FriendsQuery { user { friendsBackward: friends(last: 2) { edges { node { name } } } } } ''' expected = { 'user': { 'friendsBackward': { 'edges': [ { 'node': { 'name': 'Joe' } }, { 'node': { 'name': 'Tim' } }, ] } } } result = graphql(schema, query) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/graphql_relay/mutation/000077500000000000000000000000001357255267200214205ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/mutation/__init__.py000066400000000000000000000000001357255267200235170ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/mutation/mutation.py000066400000000000000000000032751357255267200236410ustar00rootroot00000000000000from collections import OrderedDict from promise import Promise from graphql.type import ( GraphQLArgument, GraphQLInputObjectField, GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType, GraphQLString, GraphQLField, ) from graphql.error import GraphQLError from ..utils import resolve_maybe_thunk def mutation_with_client_mutation_id( name, input_fields, output_fields, mutate_and_get_payload): augmented_input_fields = OrderedDict( resolve_maybe_thunk(input_fields), clientMutationId=GraphQLInputObjectField( GraphQLNonNull(GraphQLString) ) ) augmented_output_fields = OrderedDict( resolve_maybe_thunk(output_fields), clientMutationId=GraphQLField( GraphQLNonNull(GraphQLString) ) ) input_type = GraphQLInputObjectType( name + 'Input', fields=augmented_input_fields, ) output_type = GraphQLObjectType( name + 'Payload', fields=augmented_output_fields, ) def resolver(_root, info, **args): input_ = args.get('input') def on_resolve(payload): try: payload.clientMutationId = input_['clientMutationId'] except Exception: raise GraphQLError( 'Cannot set clientMutationId in the payload object {}'.format( repr(payload))) return payload return Promise.resolve(mutate_and_get_payload(info, **input_)).then(on_resolve) return GraphQLField( output_type, args=OrderedDict(( ('input', GraphQLArgument(GraphQLNonNull(input_type))), )), resolver=resolver ) graphql-relay-py-2.0.1/graphql_relay/mutation/tests/000077500000000000000000000000001357255267200225625ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/mutation/tests/__init__.py000066400000000000000000000000001357255267200246610ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/mutation/tests/test_mutation.py000066400000000000000000000265151357255267200260440ustar00rootroot00000000000000from promise import Promise from graphql import graphql from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLInt, GraphQLField, GraphQLInputObjectField ) from ..mutation import mutation_with_client_mutation_id class Result(object): def __init__(self, result, clientMutationId=None): self.clientMutationId = clientMutationId self.result = result simpleMutation = mutation_with_client_mutation_id( 'SimpleMutation', input_fields={}, output_fields={ 'result': GraphQLField(GraphQLInt) }, mutate_and_get_payload=lambda _info, **_input: Result(result=1) ) simpleMutationWithThunkFields = mutation_with_client_mutation_id( 'SimpleMutationWithThunkFields', input_fields=lambda: { 'inputData': GraphQLInputObjectField(GraphQLInt) }, output_fields=lambda: { 'result': GraphQLField(GraphQLInt) }, mutate_and_get_payload=lambda _info, **input_: Result(result=input_['inputData']) ) simplePromiseMutation = mutation_with_client_mutation_id( 'SimplePromiseMutation', input_fields={}, output_fields={ 'result': GraphQLField(GraphQLInt) }, mutate_and_get_payload=lambda _info, **_input: Promise.resolve(Result(result=1)) ) simpleRootValueMutation = mutation_with_client_mutation_id( 'SimpleRootValueMutation', input_fields={}, output_fields={ 'result': GraphQLField(GraphQLInt) }, mutate_and_get_payload=lambda info, **_input: info.root_value ) mutation = GraphQLObjectType( 'Mutation', fields={ 'simpleMutation': simpleMutation, 'simpleMutationWithThunkFields': simpleMutationWithThunkFields, 'simplePromiseMutation': simplePromiseMutation, 'simpleRootValueMutation': simpleRootValueMutation } ) schema = GraphQLSchema( query=mutation, mutation=mutation ) def test_requires_an_argument(): query = ''' mutation M { simpleMutation { result } } ''' result = graphql(schema, query) assert len(result.errors) == 1 def test_returns_the_same_client_mutation_id(): query = ''' mutation M { simpleMutation(input: {clientMutationId: "abc"}) { result clientMutationId } } ''' expected = { 'simpleMutation': { 'result': 1, 'clientMutationId': 'abc' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_supports_thunks_as_input_and_output_fields(): query = ''' mutation M { simpleMutationWithThunkFields( input: {inputData: 1234, clientMutationId: "abc"}) { result clientMutationId } } ''' expected = { 'simpleMutationWithThunkFields': { 'result': 1234, 'clientMutationId': 'abc' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_supports_promise_mutations(): query = ''' mutation M { simplePromiseMutation(input: {clientMutationId: "abc"}) { result clientMutationId } } ''' expected = { 'simplePromiseMutation': { 'result': 1, 'clientMutationId': 'abc' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_can_access_root_value(): query = ''' mutation M { simpleRootValueMutation(input: {clientMutationId: "abc"}) { result clientMutationId } } ''' expected = { 'simpleRootValueMutation': { 'result': 1, 'clientMutationId': 'abc' } } result = graphql(schema, query, root=Result(result=1)) assert not result.errors assert result.data == expected def test_contains_correct_input(): query = ''' { __type(name: "SimpleMutationInput") { name kind inputFields { name type { name kind ofType { name kind } } } } } ''' expected = { '__type': { 'name': 'SimpleMutationInput', 'kind': 'INPUT_OBJECT', 'inputFields': [ { 'name': 'clientMutationId', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'String', 'kind': 'SCALAR' } } } ] } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_contains_correct_payload(): query = ''' { __type(name: "SimpleMutationPayload") { name kind fields { name type { name kind ofType { name kind } } } } } ''' expected1 = { '__type': { 'name': 'SimpleMutationPayload', 'kind': 'OBJECT', 'fields': [ { 'name': 'clientMutationId', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'String', 'kind': 'SCALAR' } } }, { 'name': 'result', 'type': { 'name': 'Int', 'kind': 'SCALAR', 'ofType': None } }, ] } } expected2 = { '__type': { 'name': 'SimpleMutationPayload', 'kind': 'OBJECT', 'fields': [ { 'name': 'result', 'type': { 'name': 'Int', 'kind': 'SCALAR', 'ofType': None } }, { 'name': 'clientMutationId', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'String', 'kind': 'SCALAR' } } }, ] } } result = graphql(schema, query) assert not result.errors assert result.data == expected1 or result.data == expected2 def test_contains_correct_field(): query = ''' { __schema { mutationType { fields { name args { name type { name kind ofType { name kind } } } type { name kind } } } } } ''' expected = { '__schema': { 'mutationType': { 'fields': [ { 'name': 'simplePromiseMutation', 'args': [ { 'name': 'input', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'SimplePromiseMutationInput', 'kind': 'INPUT_OBJECT' } }, } ], 'type': { 'name': 'SimplePromiseMutationPayload', 'kind': 'OBJECT', } }, { 'name': 'simpleRootValueMutation', 'args': [ { 'name': 'input', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'SimpleRootValueMutationInput', 'kind': 'INPUT_OBJECT' } }, } ], 'type': { 'name': 'SimpleRootValueMutationPayload', 'kind': 'OBJECT', } }, { 'name': 'simpleMutation', 'args': [ { 'name': 'input', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'SimpleMutationInput', 'kind': 'INPUT_OBJECT' } }, } ], 'type': { 'name': 'SimpleMutationPayload', 'kind': 'OBJECT', } }, { 'name': 'simpleMutationWithThunkFields', 'args': [ { 'name': 'input', 'type': { 'name': None, 'kind': 'NON_NULL', 'ofType': { 'name': 'SimpleMutationWithThunkFieldsInput', 'kind': 'INPUT_OBJECT' } }, } ], 'type': { 'name': 'SimpleMutationWithThunkFieldsPayload', 'kind': 'OBJECT', } }, ] } } } result = graphql(schema, query) assert not result.errors # ensure the ordering is correct for the assertion expected['__schema']['mutationType']['fields'] = sorted( expected['__schema']['mutationType']['fields'], key=lambda k: k['name'] ) result.data['__schema']['mutationType']['fields'] = sorted( result.data['__schema']['mutationType']['fields'], key=lambda k: k['name'] ) assert result.data == expected graphql-relay-py-2.0.1/graphql_relay/node/000077500000000000000000000000001357255267200205055ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/node/__init__.py000066400000000000000000000000001357255267200226040ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/node/node.py000066400000000000000000000052121357255267200220040ustar00rootroot00000000000000from collections import OrderedDict from six import text_type from graphql.type import ( GraphQLArgument, GraphQLNonNull, GraphQLID, GraphQLField, GraphQLInterfaceType, ) from ..utils import base64, unbase64 def node_definitions(id_fetcher, type_resolver=None, id_resolver=None): """ Given a function to map from an ID to an underlying object, and a function to map from an underlying object to the concrete GraphQLObjectType it corresponds to, constructs a `Node` interface that objects can implement, and a field config for a `node` root field. If the type_resolver is omitted, object resolution on the interface will be handled with the `isTypeOf` method on object types, as with any GraphQL interface without a provided `resolveType` method. """ node_interface = GraphQLInterfaceType( 'Node', description='An object with an ID', fields=lambda: OrderedDict(( ('id', GraphQLField( GraphQLNonNull(GraphQLID), description='The id of the object.', resolver=id_resolver, )), )), resolve_type=type_resolver ) node_field = GraphQLField( node_interface, description='Fetches an object given its ID', args=OrderedDict(( ('id', GraphQLArgument( GraphQLNonNull(GraphQLID), description='The ID of an object' )), )), resolver=lambda _obj, info, id: id_fetcher(id, info) ) return node_interface, node_field def to_global_id(type, id): """ Takes a type name and an ID specific to that type name, and returns a "global ID" that is unique among all types. """ return base64(':'.join([type, text_type(id)])) def from_global_id(global_id): """ Takes the "global ID" created by toGlobalID, and returns the type name and ID used to create it. """ unbased_global_id = unbase64(global_id) _type, _id = unbased_global_id.split(':', 1) return _type, _id def global_id_field(type_name, id_fetcher=None): """ Creates the configuration for an id field on a node, using `to_global_id` to construct the ID from the provided typename. The type-specific ID is fetcher by calling id_fetcher on the object, or if not provided, by accessing the `id` property on the object. """ return GraphQLField( GraphQLNonNull(GraphQLID), description='The ID of an object', resolver=lambda obj, info, **args: to_global_id( type_name or info.parent_type.name, id_fetcher(obj, info) if id_fetcher else obj.id ) ) graphql-relay-py-2.0.1/graphql_relay/node/plural.py000066400000000000000000000014611357255267200223600ustar00rootroot00000000000000from collections import OrderedDict from promise import Promise from graphql.type import ( GraphQLArgument, GraphQLList, GraphQLNonNull, GraphQLField ) def plural_identifying_root_field( arg_name, input_type, output_type, resolve_single_input, description=None): input_args = OrderedDict() input_args[arg_name] = GraphQLArgument( GraphQLNonNull( GraphQLList( GraphQLNonNull(input_type) ) ) ) def resolver(_obj, info, **args): inputs = args[arg_name] return Promise.all([ resolve_single_input(info, input_) for input_ in inputs ]) return GraphQLField( GraphQLList(output_type), description=description, args=input_args, resolver=resolver ) graphql-relay-py-2.0.1/graphql_relay/node/tests/000077500000000000000000000000001357255267200216475ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/node/tests/__init__.py000066400000000000000000000000001357255267200237460ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/node/tests/test_global.py000066400000000000000000000054431357255267200245260ustar00rootroot00000000000000from collections import namedtuple from graphql import graphql from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLList, GraphQLInt, GraphQLString, ) from graphql_relay.node.node import ( from_global_id, global_id_field, node_definitions, ) User = namedtuple('User', ['id', 'name']) Photo = namedtuple('Photo', ['photoId', 'width']) userData = { '1': User(id=1, name='John Doe'), '2': User(id=2, name='Jane Smith'), } photoData = { '1': Photo(photoId=1, width=300), '2': Photo(photoId=2, width=400), } def get_node(global_id, _info): _type, _id = from_global_id(global_id) if _type == 'User': return userData[_id] else: return photoData[_id] def get_node_type(obj, _info): if isinstance(obj, User): return userType else: return photoType node_interface, node_field = node_definitions(get_node, get_node_type) userType = GraphQLObjectType( 'User', fields=lambda: { 'id': global_id_field('User'), 'name': GraphQLField(GraphQLString), }, interfaces=[node_interface] ) photoType = GraphQLObjectType( 'Photo', fields=lambda: { 'id': global_id_field('Photo', lambda obj, *_: obj.photoId), 'width': GraphQLField(GraphQLInt), }, interfaces=[node_interface] ) queryType = GraphQLObjectType( 'Query', fields=lambda: { 'node': node_field, 'allObjects': GraphQLField( GraphQLList(node_interface), resolver=lambda _root, _info: [userData['1'], userData['2'], photoData['1'], photoData['2']] ) } ) schema = GraphQLSchema( query=queryType, types=[userType, photoType] ) def test_gives_different_ids(): query = ''' { allObjects { id } } ''' expected = { 'allObjects': [ { 'id': 'VXNlcjox' }, { 'id': 'VXNlcjoy' }, { 'id': 'UGhvdG86MQ==' }, { 'id': 'UGhvdG86Mg==' }, ] } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_refetches_the_ids(): query = ''' { user: node(id: "VXNlcjox") { id ... on User { name } }, photo: node(id: "UGhvdG86MQ==") { id ... on Photo { width } } } ''' expected = { 'user': { 'id': 'VXNlcjox', 'name': 'John Doe' }, 'photo': { 'id': 'UGhvdG86MQ==', 'width': 300 } } result = graphql(schema, query) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/graphql_relay/node/tests/test_node.py000066400000000000000000000162121357255267200242070ustar00rootroot00000000000000from collections import namedtuple from graphql import graphql from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLNonNull, GraphQLInt, GraphQLString, GraphQLID, ) from ..node import node_definitions, to_global_id, from_global_id User = namedtuple('User', ['id', 'name']) Photo = namedtuple('Photo', ['id', 'width']) userData = { '1': User(id='1', name='John Doe'), '2': User(id='2', name='Jane Smith'), } photoData = { '3': Photo(id='3', width=300), '4': Photo(id='4', width=400), } def get_node(id, info): assert info.schema == schema if id in userData: return userData.get(id) else: return photoData.get(id) def get_node_type(obj, _info): if obj.id in userData: return userType else: return photoType node_interface, node_field = node_definitions(get_node, get_node_type) userType = GraphQLObjectType( 'User', fields=lambda: { 'id': GraphQLField(GraphQLNonNull(GraphQLID)), 'name': GraphQLField(GraphQLString), }, interfaces=[node_interface] ) photoType = GraphQLObjectType( 'Photo', fields=lambda: { 'id': GraphQLField(GraphQLNonNull(GraphQLID)), 'width': GraphQLField(GraphQLInt), }, interfaces=[node_interface] ) queryType = GraphQLObjectType( 'Query', fields=lambda: { 'node': node_field, } ) schema = GraphQLSchema( query=queryType, types=[userType, photoType] ) def test_gets_the_correct_id_for_users(): query = ''' { node(id: "1") { id } } ''' expected = { 'node': { 'id': '1', } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_gets_the_correct_id_for_photos(): query = ''' { node(id: "4") { id } } ''' expected = { 'node': { 'id': '4', } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_gets_the_correct_name_for_users(): query = ''' { node(id: "1") { id ... on User { name } } } ''' expected = { 'node': { 'id': '1', 'name': 'John Doe' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_gets_the_correct_width_for_photos(): query = ''' { node(id: "4") { id ... on Photo { width } } } ''' expected = { 'node': { 'id': '4', 'width': 400 } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_gets_the_correct_typename_for_users(): query = ''' { node(id: "1") { id __typename } } ''' expected = { 'node': { 'id': '1', '__typename': 'User' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_gets_the_correct_typename_for_photos(): query = ''' { node(id: "4") { id __typename } } ''' expected = { 'node': { 'id': '4', '__typename': 'Photo' } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_ignores_photo_fragments_on_user(): query = ''' { node(id: "1") { id ... on Photo { width } } } ''' expected = { 'node': { 'id': '1', } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_returns_null_for_bad_ids(): query = ''' { node(id: "5") { id } } ''' expected = { 'node': None } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_have_correct_node_interface(): query = ''' { __type(name: "Node") { name kind fields { name type { kind ofType { name kind } } } } } ''' expected = { '__type': { 'name': 'Node', 'kind': 'INTERFACE', 'fields': [ { 'name': 'id', 'type': { 'kind': 'NON_NULL', 'ofType': { 'name': 'ID', 'kind': 'SCALAR' } } } ] } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_has_correct_node_root_field(): query = ''' { __schema { queryType { fields { name type { name kind } args { name type { kind ofType { name kind } } } } } } } ''' expected = { '__schema': { 'queryType': { 'fields': [ { 'name': 'node', 'type': { 'name': 'Node', 'kind': 'INTERFACE' }, 'args': [ { 'name': 'id', 'type': { 'kind': 'NON_NULL', 'ofType': { 'name': 'ID', 'kind': 'SCALAR' } } } ] } ] } } } result = graphql(schema, query) assert not result.errors assert result.data == expected def test_to_global_id_converts_unicode_strings_correctly(): my_unicode_id = u'\xfb\xf1\xf6' g_id = to_global_id('MyType', my_unicode_id) assert g_id == 'TXlUeXBlOsO7w7HDtg==' my_unicode_id = u'\u06ED' g_id = to_global_id('MyType', my_unicode_id) assert g_id == 'TXlUeXBlOtut' def test_from_global_id_converts_unicode_strings_correctly(): my_unicode_id = u'\xfb\xf1\xf6' my_type, my_id = from_global_id('TXlUeXBlOsO7w7HDtg==') assert my_type == 'MyType' assert my_id == my_unicode_id my_unicode_id = u'\u06ED' my_type, my_id = from_global_id('TXlUeXBlOtut') assert my_type == 'MyType' assert my_id == my_unicode_id graphql-relay-py-2.0.1/graphql_relay/node/tests/test_plural.py000066400000000000000000000074121357255267200245630ustar00rootroot00000000000000from collections import namedtuple from graphql import graphql from graphql.type import ( GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString, ) from graphql_relay.node.plural import plural_identifying_root_field userType = GraphQLObjectType( 'User', fields=lambda: { 'username': GraphQLField(GraphQLString), 'url': GraphQLField(GraphQLString), } ) User = namedtuple('User', ['username', 'url']) queryType = GraphQLObjectType( 'Query', fields=lambda: { 'usernames': plural_identifying_root_field( 'usernames', description='Map from a username to the user', input_type=GraphQLString, output_type=userType, resolve_single_input=lambda info, username: User( username=username, url='www.facebook.com/' + username + '?lang=' + info.root_value.lang ) ) } ) class RootValue: lang = 'en' schema = GraphQLSchema(query=queryType) def test_allows_fetching(): query = ''' { usernames(usernames:["dschafer", "leebyron", "schrockn"]) { username url } } ''' expected = { 'usernames': [ { 'username': 'dschafer', 'url': 'www.facebook.com/dschafer?lang=en' }, { 'username': 'leebyron', 'url': 'www.facebook.com/leebyron?lang=en' }, { 'username': 'schrockn', 'url': 'www.facebook.com/schrockn?lang=en' }, ] } result = graphql(schema, query, root=RootValue()) assert not result.errors assert result.data == expected def test_correctly_introspects(): query = ''' { __schema { queryType { fields { name args { name type { kind ofType { kind ofType { kind ofType { name kind } } } } } type { kind ofType { name kind } } } } } } ''' expected = { '__schema': { 'queryType': { 'fields': [ { 'name': 'usernames', 'args': [ { 'name': 'usernames', 'type': { 'kind': 'NON_NULL', 'ofType': { 'kind': 'LIST', 'ofType': { 'kind': 'NON_NULL', 'ofType': { 'name': 'String', 'kind': 'SCALAR', } } } } } ], 'type': { 'kind': 'LIST', 'ofType': { 'name': 'User', 'kind': 'OBJECT', } } } ] } } } result = graphql(schema, query) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/graphql_relay/tests/000077500000000000000000000000001357255267200207225ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/tests/__init__.py000066400000000000000000000000001357255267200230210ustar00rootroot00000000000000graphql-relay-py-2.0.1/graphql_relay/tests/test_utils.py000066400000000000000000000017241357255267200234770ustar00rootroot00000000000000import base64 from .. import utils def test_base64_encode_unicode_strings_correctly(): my_unicode = u'\xfb\xf1\xf6' my_base64 = utils.base64(my_unicode) assert my_base64 == base64.b64encode(my_unicode.encode('utf-8')).decode('utf-8') my_unicode = u'\u06ED' my_base64 = utils.base64(my_unicode) assert my_base64 == base64.b64encode(my_unicode.encode('utf-8')).decode('utf-8') def test_base64_encode_strings_correctly(): my_string = 'abc' my_base64 = utils.base64(my_string) assert my_base64 == base64.b64encode(my_string.encode('utf-8')).decode('utf-8') def test_unbase64_decodes_unicode_strings_correctly(): my_unicode = u'\xfb\xf1\xf6' my_converted_unicode = utils.unbase64(utils.base64(my_unicode)) assert my_unicode == my_converted_unicode def test_unbase64_decodes_strings_correctly(): my_string = 'abc' my_converted_string = utils.unbase64(utils.base64(my_string)) assert my_string == my_converted_string graphql-relay-py-2.0.1/graphql_relay/utils.py000066400000000000000000000005521357255267200212740ustar00rootroot00000000000000from base64 import b64encode as _base64, b64decode as _unbase64 from six import string_types def base64(s): return _base64(s.encode('utf-8')).decode('utf-8') def unbase64(s): return _unbase64(s).decode('utf-8') def is_str(s): return isinstance(s, string_types) def resolve_maybe_thunk(f): if callable(f): return f() return f graphql-relay-py-2.0.1/setup.py000066400000000000000000000035641357255267200164500ustar00rootroot00000000000000import sys from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = "" def run_tests(self): import shlex # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(shlex.split(self.pytest_args)) sys.exit(errno) setup( name='graphql-relay', version='2.0.1', description='Relay implementation for Python', long_description=open('README.md').read(), long_description_content_type="text/markdown", url='https://github.com/graphql-python/graphql-relay-py', author='Syrus Akbary', author_email='me@syrusakbary.com', license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', "License :: OSI Approved :: MIT License", 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: PyPy', ], keywords='api graphql protocol rest relay', packages=find_packages(exclude=['tests']), install_requires=[ 'six>=1.12', 'graphql-core>=2.2,<3', 'promise>=2.2,<3' ], tests_require=['pytest>=4.6,<5', 'pytest-cov>=2.7,<3'], extras_require={ }, cmdclass={'test': PyTest}, ) graphql-relay-py-2.0.1/tests/000077500000000000000000000000001357255267200160705ustar00rootroot00000000000000graphql-relay-py-2.0.1/tests/starwars/000077500000000000000000000000001357255267200177365ustar00rootroot00000000000000graphql-relay-py-2.0.1/tests/starwars/__init__.py000066400000000000000000000000001357255267200220350ustar00rootroot00000000000000graphql-relay-py-2.0.1/tests/starwars/data.py000066400000000000000000000035101357255267200212200ustar00rootroot00000000000000"""This defines a basic set of data for our Star Wars Schema. This data is hard coded for the sake of the demo, but you could imagine fetching this data from a backend service rather than from hardcoded JSON objects in a more complex demo. """ from collections import namedtuple Ship = namedtuple('Ship', ['id', 'name']) Faction = namedtuple('Faction', ['id', 'name', 'ships']) xwing = Ship( id='1', name='X-Wing', ) ywing = Ship( id='2', name='Y-Wing', ) awing = Ship( id='3', name='A-Wing', ) # Yeah, technically it's Corellian. But it flew in the service of the rebels, # so for the purposes of this demo it's a rebel ship. falcon = Ship( id='4', name='Millenium Falcon', ) homeOne = Ship( id='5', name='Home One', ) tieFighter = Ship( id='6', name='TIE Fighter', ) tieInterceptor = Ship( id='7', name='TIE Interceptor', ) executor = Ship( id='8', name='Executor', ) rebels = Faction( id='1', name='Alliance to Restore the Republic', ships=['1', '2', '3', '4', '5'] ) empire = Faction( id='2', name='Galactic Empire', ships=['6', '7', '8'] ) data = { 'Faction': { '1': rebels, '2': empire }, 'Ship': { '1': xwing, '2': ywing, '3': awing, '4': falcon, '5': homeOne, '6': tieFighter, '7': tieInterceptor, '8': executor } } def createShip(shipName, factionId): nextShip = len(data['Ship']) + 1 newShip = Ship( id=str(nextShip), name=shipName ) data['Ship'][newShip.id] = newShip data['Faction'][factionId].ships.append(newShip.id) return newShip def getShip(_id): return data['Ship'][_id] def getFaction(_id): return data['Faction'][_id] def getRebels(): return rebels def getEmpire(): return empire graphql-relay-py-2.0.1/tests/starwars/schema.py000066400000000000000000000160011357255267200215460ustar00rootroot00000000000000from graphql.type import ( GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLInputObjectField, GraphQLSchema, GraphQLString, GraphQLField ) from graphql_relay.node.node import ( node_definitions, global_id_field, from_global_id ) from graphql_relay.connection.arrayconnection import ( connection_from_list ) from graphql_relay.connection.connection import ( connection_args, connection_definitions ) from graphql_relay.mutation.mutation import ( mutation_with_client_mutation_id ) from .data import ( Faction, getFaction, getShip, getRebels, getEmpire, createShip, ) # This is a basic end-to-end test, designed to demonstrate the various # capabilities of a Relay-compliant GraphQL server. # # It is recommended that readers of this test be familiar with # the end-to-end test in GraphQL.js first, as this test skips # over the basics covered there in favor of illustrating the # key aspects of the Relay spec that this test is designed to illustrate. # # We will create a GraphQL schema that describes the major # factions and ships in the original Star Wars trilogy. # # NOTE: This may contain spoilers for the original Star # Wars trilogy. # Using our shorthand to describe type systems, the type system for our # example will be the following: # # interface Node { # id: ID! # } # # type Faction : Node { # id: ID! # name: String # ships: ShipConnection # } # # type Ship : Node { # id: ID! # name: String # } # # type ShipConnection { # edges: [ShipEdge] # pageInfo: PageInfo! # } # # type ShipEdge { # cursor: String! # node: Ship # } # # type PageInfo { # hasNextPage: Boolean! # hasPreviousPage: Boolean! # startCursor: String # endCursor: String # } # # type Query { # rebels: Faction # empire: Faction # node(id: ID!): Node # } # # input IntroduceShipInput { # clientMutationId: string! # shipName: string! # factionId: ID! # } # # input IntroduceShipPayload { # clientMutationId: string! # ship: Ship # faction: Faction # } # # type Mutation { # introduceShip(input IntroduceShipInput!): IntroduceShipPayload # } # We get the node interface and field from the relay library. # # The first method is the way we resolve an ID to its object. The second is the # way we resolve an object that implements node to its type. def get_node(global_id, _info): type_, id_ = from_global_id(global_id) if type_ == 'Faction': return getFaction(id_) elif type_ == 'Ship': return getShip(id_) else: return None def get_node_type(obj, _info): if isinstance(obj, Faction): return factionType else: return shipType node_interface, node_field = node_definitions(get_node, get_node_type) # We define our basic ship type. # # This implements the following type system shorthand: # type Ship : Node { # id: String! # name: String # } shipType = GraphQLObjectType( name='Ship', description='A ship in the Star Wars saga', fields=lambda: { 'id': global_id_field('Ship'), 'name': GraphQLField( GraphQLString, description='The name of the ship.', ) }, interfaces=[node_interface] ) # We define a connection between a faction and its ships. # # connection_type implements the following type system shorthand: # type ShipConnection { # edges: [ShipEdge] # pageInfo: PageInfo! # } # # connection_type has an edges field - a list of edgeTypes that implement the # following type system shorthand: # type ShipEdge { # cursor: String! # node: Ship # } shipEdge, shipConnection = connection_definitions('Ship', shipType) # We define our faction type, which implements the node interface. # # This implements the following type system shorthand: # type Faction : Node { # id: String! # name: String # ships: ShipConnection # } factionType = GraphQLObjectType( name='Faction', description='A faction in the Star Wars saga', fields=lambda: { 'id': global_id_field('Faction'), 'name': GraphQLField( GraphQLString, description='The name of the faction.', ), 'ships': GraphQLField( shipConnection, description='The ships used by the faction.', args=connection_args, resolver=lambda faction, _info, **args: connection_from_list( [getShip(ship) for ship in faction.ships], args ), ) }, interfaces=[node_interface] ) # This is the type that will be the root of our query, and the # entry point into our schema. # # This implements the following type system shorthand: # type Query { # rebels: Faction # empire: Faction # node(id: String!): Node # } queryType = GraphQLObjectType( name='Query', fields=lambda: { 'rebels': GraphQLField( factionType, resolver=lambda _obj, _info: getRebels(), ), 'empire': GraphQLField( factionType, resolver=lambda _obj, _info: getEmpire(), ), 'node': node_field } ) # This will return a GraphQLFieldConfig for our ship # mutation. # # It creates these two types implicitly: # input IntroduceShipInput { # clientMutationId: string! # shipName: string! # factionId: ID! # } # # input IntroduceShipPayload { # clientMutationId: string! # ship: Ship # faction: Faction # } class IntroduceShipMutation(object): def __init__(self, shipId, factionId, clientMutationId=None): self.shipId = shipId self.factionId = factionId self.clientMutationId = clientMutationId def mutate_and_get_payload(_info, shipName, factionId, **_input): newShip = createShip(shipName, factionId) return IntroduceShipMutation( shipId=newShip.id, factionId=factionId, ) shipMutation = mutation_with_client_mutation_id( 'IntroduceShip', input_fields={ 'shipName': GraphQLInputObjectField( GraphQLNonNull(GraphQLString) ), 'factionId': GraphQLInputObjectField( GraphQLNonNull(GraphQLID) ) }, output_fields={ 'ship': GraphQLField( shipType, resolver=lambda payload, _info: getShip(payload.shipId) ), 'faction': GraphQLField( factionType, resolver=lambda payload, _info: getFaction(payload.factionId) ) }, mutate_and_get_payload=mutate_and_get_payload ) # This is the type that will be the root of our mutations, and the # entry point into performing writes in our schema. # # This implements the following type system shorthand: # type Mutation { # introduceShip(input IntroduceShipInput!): IntroduceShipPayload # } mutationType = GraphQLObjectType( 'Mutation', fields=lambda: { 'introduceShip': shipMutation } ) # Finally, we construct our schema (whose starting query type is the query # type we defined above) and export it. StarWarsSchema = GraphQLSchema( query=queryType, mutation=mutationType ) graphql-relay-py-2.0.1/tests/starwars/test_connections.py000066400000000000000000000014041357255267200236700ustar00rootroot00000000000000from graphql import graphql from .schema import StarWarsSchema def test_correct_fetch_first_ship_rebels(): query = ''' query RebelsShipsQuery { rebels { name, ships(first: 1) { edges { node { name } } } } } ''' expected = { 'rebels': { 'name': 'Alliance to Restore the Republic', 'ships': { 'edges': [ { 'node': { 'name': 'X-Wing' } } ] } } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/tests/starwars/test_mutations.py000066400000000000000000000017071357255267200233770ustar00rootroot00000000000000from graphql import graphql from .schema import StarWarsSchema def test_correctly_mutates_dataset(): query = ''' mutation AddBWingQuery($input: IntroduceShipInput!) { introduceShip(input: $input) { ship { id name } faction { name } clientMutationId } } ''' params = { 'input': { 'shipName': 'B-Wing', 'factionId': '1', 'clientMutationId': 'abcde', } } expected = { 'introduceShip': { 'ship': { 'id': 'U2hpcDo5', 'name': 'B-Wing' }, 'faction': { 'name': 'Alliance to Restore the Republic' }, 'clientMutationId': 'abcde', } } result = graphql(StarWarsSchema, query, variables=params) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/tests/starwars/test_objectidentification.py000066400000000000000000000042471357255267200255360ustar00rootroot00000000000000from graphql import graphql from .schema import StarWarsSchema def test_correctly_fetches_id_name_rebels(): query = ''' query RebelsQuery { rebels { id name } } ''' expected = { 'rebels': { 'id': 'RmFjdGlvbjox', 'name': 'Alliance to Restore the Republic' } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected def test_correctly_refetches_rebels(): query = ''' query RebelsRefetchQuery { node(id: "RmFjdGlvbjox") { id ... on Faction { name } } } ''' expected = { 'node': { 'id': 'RmFjdGlvbjox', 'name': 'Alliance to Restore the Republic' } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected def test_correctly_fetches_id_name_empire(): query = ''' query EmpireQuery { empire { id name } } ''' expected = { 'empire': { 'id': 'RmFjdGlvbjoy', 'name': 'Galactic Empire' } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected def test_correctly_refetches_empire(): query = ''' query EmpireRefetchQuery { node(id: "RmFjdGlvbjoy") { id ... on Faction { name } } } ''' expected = { 'node': { 'id': 'RmFjdGlvbjoy', 'name': 'Galactic Empire' } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected def test_correctly_refetches_xwing(): query = ''' query XWingRefetchQuery { node(id: "U2hpcDox") { id ... on Ship { name } } } ''' expected = { 'node': { 'id': 'U2hpcDox', 'name': 'X-Wing' } } result = graphql(StarWarsSchema, query) assert not result.errors assert result.data == expected graphql-relay-py-2.0.1/tox.ini000066400000000000000000000006461357255267200162470ustar00rootroot00000000000000[tox] envlist = py{27,34,35,36,37,38,py,py3}, flake8, manifest [testenv:flake8] basepython = python3.7 deps = flake8>=3.7,<4 commands = flake8 setup.py graphql_relay tests [testenv:pypy] whitelist_externals=* [testenv:pypy3] whitelist_externals=* [testenv:manifest] basepython = python3.7 deps = check-manifest>=0.40,<1 commands = check-manifest -v [testenv] commands= python setup.py test -a "{posargs}"