Repository: mirumee/ariadne

Issue:
Pass operation name and variables to root value resolver
Split form #843

Callable root value resolver becomes really useful in scenarios like GraphQL proxies, but it should have easy access to operation name and variables dict for that.

Plan is to change root value callable to support those as extra arguments, effectively changing type to that:

```python
RootValue = Union[
    Any,
    Callable[[Optional[Any], DocumentNode], Any],
    Callable[[Optional[Any], Optional[str], Optional[dict], DocumentNode], Any],
]
```


Discussion:

Resolving patch:
diff --git a/ariadne/graphql.py b/ariadne/graphql.py
--- a/ariadne/graphql.py
+++ b/ariadne/graphql.py
@@ -13,6 +13,7 @@
     cast,
     Union,
 )
+from warnings import warn
 
 from graphql import (
     DocumentNode,
@@ -46,6 +47,16 @@
 from .validation.introspection_disabled import IntrospectionDisabledRule
 
 
+def root_value_two_args_deprecated():  # TODO: remove in 0.19
+    warn(
+        "'root_value(context, document)' has been deprecated and will raise a type "
+        "error in Ariadne 0.18, update definition to "
+        "'root_value(context, operation_name, variables, document)'.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+
+
 async def graphql(
     schema: GraphQLSchema,
     data: Any,
@@ -99,7 +110,14 @@ async def graphql(
                 )
 
             if callable(root_value):
-                root_value = root_value(context_value, document)
+                try:
+                    root_value = root_value(  # type: ignore
+                        context_value, operation_name, variables, document
+                    )
+                except TypeError:  # TODO: remove in 0.19
+                    root_value_two_args_deprecated()
+                    root_value = root_value(context_value, document)  # type: ignore
+
                 if isawaitable(root_value):
                     root_value = await root_value
 
@@ -190,7 +208,14 @@ def graphql_sync(
                 )
 
             if callable(root_value):
-                root_value = root_value(context_value, document)
+                try:
+                    root_value = root_value(  # type: ignore
+                        context_value, operation_name, variables, document
+                    )
+                except TypeError:  # TODO: remove in 0.19
+                    root_value_two_args_deprecated()
+                    root_value = root_value(context_value, document)  # type: ignore
+
                 if isawaitable(root_value):
                     ensure_future(root_value).cancel()
                     raise RuntimeError(
@@ -280,7 +305,14 @@ async def subscribe(
             )
 
         if callable(root_value):
-            root_value = root_value(context_value, document)
+            try:
+                root_value = root_value(  # type: ignore
+                    context_value, operation_name, variables, document
+                )
+            except TypeError:  # TODO: remove in 0.19
+                root_value_two_args_deprecated()
+                root_value = root_value(context_value, document)  # type: ignore
+
             if isawaitable(root_value):
                 root_value = await root_value
 
diff --git a/ariadne/types.py b/ariadne/types.py
--- a/ariadne/types.py
+++ b/ariadne/types.py
@@ -40,7 +40,11 @@
 ErrorFormatter = Callable[[GraphQLError, bool], dict]
 
 ContextValue = Union[Any, Callable[[Any], Any]]
-RootValue = Union[Any, Callable[[Optional[Any], DocumentNode], Any]]
+RootValue = Union[
+    Any,
+    Callable[[Optional[Any], DocumentNode], Any],  # TODO: remove in 0.19
+    Callable[[Optional[Any], Optional[str], Optional[dict], DocumentNode], Any],
+]
 
 QueryParser = Callable[[ContextValue, Dict[str, Any]], DocumentNode]
 


Test patch:
diff --git a/tests/asgi/test_configuration.py b/tests/asgi/test_configuration.py
--- a/tests/asgi/test_configuration.py
+++ b/tests/asgi/test_configuration.py
@@ -111,6 +111,21 @@ def test_custom_root_value_function_is_called_by_query(schema):
     get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_query(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(
+        schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
+    )
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        client.post("/", json={"query": "{ status }"})
+
+
 def test_custom_root_value_function_is_called_by_subscription(schema):
     get_root_value = Mock(return_value=True)
     app = GraphQL(schema, root_value=get_root_value)
@@ -131,6 +146,29 @@ def test_custom_root_value_function_is_called_by_subscription(schema):
         get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_subscription(schema):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(schema, root_value=get_root_value)
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        with client.websocket_connect("/", ["graphql-ws"]) as ws:
+            ws.send_json({"type": GraphQLWSHandler.GQL_CONNECTION_INIT})
+            ws.send_json(
+                {
+                    "type": GraphQLWSHandler.GQL_START,
+                    "id": "test1",
+                    "payload": {"query": "subscription { ping }"},
+                }
+            )
+            response = ws.receive_json()
+            assert response["type"] == GraphQLWSHandler.GQL_CONNECTION_ACK
+            response = ws.receive_json()
+            assert response["type"] == GraphQLWSHandler.GQL_DATA
+
+
 def test_custom_root_value_function_is_called_by_subscription_graphql_transport_ws(
     schema,
 ):
@@ -156,6 +194,34 @@ def test_custom_root_value_function_is_called_by_subscription_graphql_transport_
         get_root_value.assert_called_once()
 
 
+def test_custom_deprecated_root_value_function_raises_warning_by_subscription_graphql_transport_ws(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    websocket_handler = GraphQLTransportWSHandler()
+    app = GraphQL(
+        schema, root_value=get_root_value, websocket_handler=websocket_handler
+    )
+    client = TestClient(app)
+
+    with pytest.deprecated_call():
+        with client.websocket_connect("/", ["graphql-transport-ws"]) as ws:
+            ws.send_json({"type": GraphQLTransportWSHandler.GQL_CONNECTION_INIT})
+            ws.send_json(
+                {
+                    "type": GraphQLTransportWSHandler.GQL_SUBSCRIBE,
+                    "id": "test1",
+                    "payload": {"query": "subscription { ping }"},
+                }
+            )
+            response = ws.receive_json()
+            assert response["type"] == GraphQLTransportWSHandler.GQL_CONNECTION_ACK
+            response = ws.receive_json()
+            assert response["type"] == GraphQLTransportWSHandler.GQL_NEXT
+
+
 def test_custom_root_value_function_is_called_with_context_value(schema):
     get_root_value = Mock(return_value=True)
     app = GraphQL(
@@ -163,7 +229,7 @@ def test_custom_root_value_function_is_called_with_context_value(schema):
     )
     client = TestClient(app)
     client.post("/", json={"query": "{ status }"})
-    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, ANY)
+    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, None, None, ANY)
 
 
 def test_custom_query_parser_is_used_for_http_query(schema):
diff --git a/tests/wsgi/test_configuration.py b/tests/wsgi/test_configuration.py
--- a/tests/wsgi/test_configuration.py
+++ b/tests/wsgi/test_configuration.py
@@ -76,7 +76,21 @@ def test_custom_root_value_function_is_called_with_context_value(schema):
         schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
     )
     app.execute_query({}, {"query": "{ status }"})
-    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, ANY)
+    get_root_value.assert_called_once_with({"test": "TEST-CONTEXT"}, None, None, ANY)
+
+
+def test_warning_is_raised_if_custom_root_value_function_has_deprecated_signature(
+    schema,
+):
+    def get_root_value(_context, _document):
+        return True
+
+    app = GraphQL(
+        schema, context_value={"test": "TEST-CONTEXT"}, root_value=get_root_value
+    )
+
+    with pytest.deprecated_call():
+        app.execute_query({}, {"query": "{ status }"})
 
 
 def test_custom_query_parser_is_used(schema):