diff --git a/graphene/utils/deduplicator.py b/graphene/utils/deduplicator.py new file mode 100644 index 00000000..fd5682b1 --- /dev/null +++ b/graphene/utils/deduplicator.py @@ -0,0 +1,38 @@ +from collections import Mapping, OrderedDict + + +def deflate(node, index=None, path=None): + if index is None: + index = {} + if path is None: + path = [] + + if node and 'id' in node and '__typename' in node: + route = ','.join(path) + cache_key = ':'.join([route, str(node['__typename']), str(node['id'])]) + + if index.get(cache_key) is True: + return { + '__typename': node['__typename'], + 'id': node['id'], + } + else: + index[cache_key] = True + + field_names = node.keys() + result = OrderedDict() + + for field_name in field_names: + value = node[field_name] + + new_path = path + [field_name] + if isinstance(value, (list, tuple)): + result[field_name] = [ + deflate(child, index, new_path) for child in value + ] + elif isinstance(value, Mapping): + result[field_name] = deflate(value, index, new_path) + else: + result[field_name] = value + + return result diff --git a/graphene/utils/tests/test_deduplicator.py b/graphene/utils/tests/test_deduplicator.py new file mode 100644 index 00000000..d73fbfbe --- /dev/null +++ b/graphene/utils/tests/test_deduplicator.py @@ -0,0 +1,257 @@ +import datetime +import graphene +from graphene import relay +from graphene.types.resolver import dict_resolver + +from ..deduplicator import deflate + + +def test_does_not_modify_object_without_typename_and_id(): + response = { + 'foo': 'bar', + } + + deflated_response = deflate(response) + assert deflated_response == { + 'foo': 'bar', + } + + +def test_does_not_modify_first_instance_of_an_object(): + response = { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + deflated_response = deflate(response) + + assert deflated_response == { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1 + } + ] + } + + +def test_does_not_modify_first_instance_of_an_object_nested(): + response = { + 'data': [ + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 1 + }, + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 2 + } + ] + } + + deflated_response = deflate(response) + + assert deflated_response == { + 'data': [ + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'bar2': { + '__typename': 'bar', + 'id': 1, + 'name': 'bar' + }, + 'id': 1 + }, + { + '__typename': 'foo', + 'bar1': { + '__typename': 'bar', + 'id': 1 + }, + 'bar2': { + '__typename': 'bar', + 'id': 1 + }, + 'id': 2 + } + ] + } + + +def test_does_not_modify_input(): + response = { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + deflate(response) + + assert response == { + 'data': [ + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + }, + { + '__typename': 'foo', + 'id': 1, + 'name': 'foo' + } + ] + } + + +TEST_DATA = { + 'events': [ + { + 'id': '568', + 'date': datetime.date(2017, 5, 19), + 'movie': '1198359', + }, + { + 'id': '234', + 'date': datetime.date(2017, 5, 20), + 'movie': '1198359', + }, + ], + 'movies': { + '1198359': { + 'name': 'King Arthur: Legend of the Sword', + 'synopsis': ( + "When the child Arthur's father is murdered, Vortigern, " + "Arthur's uncle, seizes the crown. Robbed of his birthright and " + "with no idea who he truly is..." + ), + }, + }, +} + + +def test_example_end_to_end(): + class Movie(graphene.ObjectType): + class Meta: + interfaces = (relay.Node,) + default_resolver = dict_resolver + + name = graphene.String(required=True) + synopsis = graphene.String(required=True) + + class Event(graphene.ObjectType): + class Meta: + interfaces = (relay.Node,) + default_resolver = dict_resolver + + movie = graphene.Field(Movie, required=True) + date = graphene.types.datetime.Date(required=True) + + def resolve_movie(event, info): + return TEST_DATA['movies'][event['movie']] + + class Query(graphene.ObjectType): + events = graphene.List( + graphene.NonNull(Event), + required=True + ) + + def resolve_events(_, info): + return TEST_DATA['events'] + + schema = graphene.Schema(query=Query) + query = """\ + { + events { + __typename + id + date + movie { + __typename + id + name + synopsis + } + } + } + """ + result = schema.execute(query) + assert not result.errors + + result.data = deflate(result.data) + assert result.data == { + 'events': [ + { + '__typename': 'Event', + 'id': 'RXZlbnQ6NTY4', + 'date': '2017-05-19', + 'movie': { + '__typename': 'Movie', + 'id': 'TW92aWU6Tm9uZQ==', + 'name': 'King Arthur: Legend of the Sword', + 'synopsis': ( + "When the child Arthur's father is murdered, Vortigern, " + "Arthur's uncle, seizes the crown. Robbed of his birthright and " + "with no idea who he truly is..." + ), + }, + }, + { + '__typename': 'Event', + 'id': 'RXZlbnQ6MjM0', + 'date': '2017-05-20', + 'movie': { + '__typename': 'Movie', + 'id': 'TW92aWU6Tm9uZQ==', + }, + }, + ], + }