Repository: mirumee/ariadne

Issue:
Federation: exception for async reference resolver
Hi all! I'm tinkering with the just-merged Federation support :tada: I'm trying to define a reference resolver that returns a coroutine (`aiodataloader`):

```py
@User.reference_resolver
def resolve_reference(type_object, info, reference):
    type, id = from_global_id(reference["id"])  # parses a Relay ID
    return info.context.users.get_by_id.load(int(id))
```

But that raises an exception in `add_typename_to_possible_return`:

```
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 343, in execute_operation
    self.execute_fields_serially
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 435, in execute_fields
    result = self.resolve_field(
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 606, in resolve_field
    return self.complete_value_catching_error(
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 683, in complete_value_catching_error
    self.handle_field_error(error, field_nodes, path, return_type)
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 698, in handle_field_error
    raise error
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 669, in complete_value_catching_error
    completed = self.complete_value(
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 735, in complete_value
    raise result
  File "/usr/local/lib/python3.8/site-packages/graphql/execution/execute.py", line 625, in resolve_field_value_or_error
    result = resolve_fn(source, info, **args)
  File "/usr/local/lib/python3.8/site-packages/ariadne/contrib/federation/utils.py", line 83, in resolve_entities
    add_typename_to_possible_return(
  File "/usr/local/lib/python3.8/site-packages/ariadne/contrib/federation/utils.py", line 132, in add_typename_to_possible_return
    setattr(obj, f"_{obj.__class__.__name__}__typename", typename)
graphql.error.graphql_error.GraphQLError: 'coroutine' object has no attribute '_coroutine__typename'
```

If I change the reference resolver to return a `dict` with some test data, it works fine, but I need to use that dataloader to do a real lookup.

Any thoughts on where the problem lies?

(also, I'm new to asyncio, and originally had `async def resolve_reference(...):` and `return await ...`, which I realized is sort of pointless. Mentioning it here in case it's relevant.)


Discussion:
@vreon thanks for pointing at this. We are aware of this problem and we plan to fix it before next Ariadne release 🚀 Stay tuned✋

Resolving patch:
diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py
--- a/ariadne/contrib/federation/utils.py
+++ b/ariadne/contrib/federation/utils.py
@@ -1,6 +1,7 @@
 # pylint: disable=cell-var-from-loop
 
 import re
+from inspect import isawaitable
 from typing import Any, List
 
 from graphql.language import DirectiveNode
@@ -79,15 +80,20 @@ def resolve_entities(_: Any, info: GraphQLResolveInfo, **kwargs) -> Any:
             type_object, "__resolve_reference__", lambda o, i, r: reference,
         )
 
-        result.append(
-            add_typename_to_possible_return(
-                resolve_reference(type_object, info, reference), __typename,
-            ),
-        )
+        representation = resolve_reference(type_object, info, reference)
+
+        if isawaitable(representation):
+            result.append(add_typename_to_async_return(representation, __typename))
+        else:
+            result.append(add_typename_to_possible_return(representation, __typename))
 
     return result
 
 
+async def add_typename_to_async_return(obj: Any, typename: str) -> Any:
+    return add_typename_to_possible_return(await obj, typename)
+
+
 def get_entity_types(schema: GraphQLSchema) -> List[GraphQLNamedType]:
     """Get all types that include the @key directive."""
     schema_types = schema.type_map.values()


Test patch:
diff --git a/tests/federation/test_schema.py b/tests/federation/test_schema.py
--- a/tests/federation/test_schema.py
+++ b/tests/federation/test_schema.py
@@ -2,7 +2,8 @@
 
 from unittest.mock import Mock
 
-from graphql import graphql_sync
+import pytest
+from graphql import graphql, graphql_sync
 from graphql.utilities import strip_ignored_characters as sic
 from graphql.utilities.schema_printer import (
     print_interface,
@@ -326,6 +327,70 @@ def user_reference_resolver(_obj, _info, reference):
     assert result.data["_entities"][1]["firstName"] == "James"
 
 
+@pytest.mark.asyncio
+async def test_federated_schema_execute_async_reference_resolver():
+    type_defs = """
+        type Query {
+            rootField: String
+        }
+
+        type Product @key(fields: "upc") {
+            upc: Int
+            name: String
+        }
+
+        type User @key(fields: "id") {
+            firstName: String
+        }
+    """
+
+    product = FederatedObjectType("Product")
+
+    @product.reference_resolver()
+    async def product_reference_resolver(_obj, _info, reference):
+        assert reference["upc"] == 1
+        return {"name": "Apollo Gateway"}
+
+    user = FederatedObjectType("User")
+
+    @user.reference_resolver()
+    async def user_reference_resolver(_obj, _info, reference):
+        assert reference["id"] == 1
+        return Mock(firstName="James")
+
+    schema = make_federated_schema(type_defs, [product, user])
+
+    result = await graphql(
+        schema,
+        """
+            query GetEntities($representations: [_Any!]!) {
+                _entities(representations: $representations) {
+                    ... on Product {
+                        __typename
+                        name
+                    }
+                    ... on User {
+                        __typename
+                        firstName
+                    }
+                }
+            }
+        """,
+        variable_values={
+            "representations": [
+                {"__typename": "Product", "upc": 1,},
+                {"__typename": "User", "id": 1,},
+            ],
+        },
+    )
+
+    assert result.errors is None
+    assert result.data["_entities"][0]["__typename"] == "Product"
+    assert result.data["_entities"][0]["name"] == "Apollo Gateway"
+    assert result.data["_entities"][1]["__typename"] == "User"
+    assert result.data["_entities"][1]["firstName"] == "James"
+
+
 def test_federated_schema_execute_default_reference_resolver():
     type_defs = """
         type Query {