Repository: edgedb/edgedb

Issue:
Change how globals are passed in GraphQL
Currently global variables are passed as their own special `globals` field. This is a non-standard field and it gets in the way of existing frameworks and libraries.

An alternative to that would be to use the `variables` field that's part of the GraphQL standard and add `__globals__` to that.


Discussion:

Resolving patch:
diff --git a/edb/testbase/http.py b/edb/testbase/http.py
--- a/edb/testbase/http.py
+++ b/edb/testbase/http.py
@@ -162,7 +162,8 @@ def get_extension_name(cls):
     def graphql_query(self, query, *, operation_name=None,
                       use_http_post=True,
                       variables=None,
-                      globals=None):
+                      globals=None,
+                      deprecated_globals=None):
         req_data = {
             'query': query
         }
@@ -174,7 +175,13 @@ def graphql_query(self, query, *, operation_name=None,
             if variables is not None:
                 req_data['variables'] = variables
             if globals is not None:
-                req_data['globals'] = globals
+                if variables is None:
+                    req_data['variables'] = dict()
+                req_data['variables']['__globals__'] = globals
+            # Support testing the old way of sending globals.
+            if deprecated_globals is not None:
+                req_data['globals'] = deprecated_globals
+
             req = urllib.request.Request(self.http_addr, method='POST')
             req.add_header('Content-Type', 'application/json')
             response = urllib.request.urlopen(
@@ -182,10 +189,15 @@ def graphql_query(self, query, *, operation_name=None,
             )
             resp_data = json.loads(response.read())
         else:
+            if globals is not None:
+                if variables is None:
+                    variables = dict()
+                variables['__globals__'] = globals
+            # Support testing the old way of sending globals.
+            if deprecated_globals is not None:
+                req_data['globals'] = json.dumps(deprecated_globals)
             if variables is not None:
                 req_data['variables'] = json.dumps(variables)
-            if globals is not None:
-                req_data['globals'] = json.dumps(globals)
             response = urllib.request.urlopen(
                 f'{self.http_addr}/?{urllib.parse.urlencode(req_data)}',
                 context=self.tls_context,
@@ -223,13 +235,15 @@ def assert_graphql_query_result(self, query, result, *,
                                     operation_name=None,
                                     use_http_post=True,
                                     variables=None,
-                                    globals=None):
+                                    globals=None,
+                                    deprecated_globals=None):
         res = self.graphql_query(
             query,
             operation_name=operation_name,
             use_http_post=use_http_post,
             variables=variables,
-            globals=globals)
+            globals=globals,
+            deprecated_globals=deprecated_globals)
 
         if sort is not None:
             # GQL will always have a single object returned. The data is


Test patch:
diff --git a/tests/test_http_graphql_query.py b/tests/test_http_graphql_query.py
--- a/tests/test_http_graphql_query.py
+++ b/tests/test_http_graphql_query.py
@@ -20,6 +20,7 @@
 import json
 import os
 import uuid
+import urllib
 
 import edgedb
 
@@ -4411,6 +4412,50 @@ def test_graphql_functional_edgedb_errors_04(self):
     def test_graphql_globals_01(self):
         Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''
 
+        for use_http_post in [True, False]:
+            self.assert_graphql_query_result(
+                Q,
+                {
+                    "GlobalTest": [{
+                        'gstr': 'WOO',
+                        'gid': '84ed3d8b-5eb2-4d31-9e1e-efb66180445c',
+                        'gdef': '',
+                        'gdef2': None, 'garray': ['x', 'y', 'z']
+                    }],
+                },
+                use_http_post=use_http_post,
+                deprecated_globals={
+                    'default::test_global_str': "WOO",
+                    'default::test_global_id': (
+                        '84ed3d8b-5eb2-4d31-9e1e-efb66180445c'),
+                    'default::test_global_def': None,
+                    'default::test_global_def2': None,
+                    'default::test_global_array': ['x', 'y', 'z'],
+                },
+            )
+
+            self.assert_graphql_query_result(
+                Q,
+                {'GlobalTest': [{'gdef': 'x', 'gdef2': 'x'}]},
+                use_http_post=use_http_post,
+                deprecated_globals={
+                    'default::test_global_def': 'x',
+                    'default::test_global_def2': 'x',
+                },
+            )
+
+            self.assert_graphql_query_result(
+                Q,
+                {'GlobalTest': [
+                    {'gstr': None, 'garray': None, 'gid': None,
+                     'gdef': '', 'gdef2': ''}
+                ]},
+                use_http_post=use_http_post,
+            )
+
+    def test_graphql_globals_02(self):
+        Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''
+
         for use_http_post in [True, False]:
             self.assert_graphql_query_result(
                 Q,
@@ -4452,6 +4497,83 @@ def test_graphql_globals_01(self):
                 use_http_post=use_http_post,
             )
 
+    def test_graphql_globals_03(self):
+        Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''
+
+        # Test that globals work fine if both the deprecated way and the new
+        # way are used and the values match.
+        for use_http_post in [True, False]:
+            self.assert_graphql_query_result(
+                Q,
+                {'GlobalTest': [{'gdef': 'x', 'gdef2': 'x'}]},
+                use_http_post=use_http_post,
+                globals={
+                    'default::test_global_def': 'x',
+                    'default::test_global_def2': 'x',
+                },
+                deprecated_globals={
+                    'default::test_global_def': 'x',
+                    'default::test_global_def2': 'x',
+                },
+            )
+
+    def test_graphql_globals_04(self):
+        Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''
+
+        # Test that globals must match if two ways are used to specify them.
+        for use_http_post in [True, False]:
+            with self.assertRaises(
+                urllib.error.HTTPError,
+            ):
+                self.assert_graphql_query_result(
+                    Q,
+                    {'GlobalTest': [{'gdef': 'x', 'gdef2': 'x'}]},
+                    use_http_post=use_http_post,
+                    globals={
+                        'default::test_global_def': 'x',
+                        'default::test_global_def2': 'x',
+                    },
+                    deprecated_globals={
+                        'default::test_global_def': 'not x',
+                        'default::test_global_def2': 'not x',
+                    },
+                )
+
+    def test_graphql_globals_05(self):
+        Q = r'''query { GlobalTest { gstr, garray, gid, gdef, gdef2 } }'''
+
+        # Test that globals must match if two ways are used to specify them,
+        # even if one of these is an empty dict.
+        for use_http_post in [True, False]:
+            with self.assertRaises(
+                urllib.error.HTTPError,
+            ):
+                self.assert_graphql_query_result(
+                    Q,
+                    {'GlobalTest': [{'gdef': 'x', 'gdef2': 'x'}]},
+                    use_http_post=use_http_post,
+                    globals={
+                        'default::test_global_def': 'x',
+                        'default::test_global_def2': 'x',
+                    },
+                    deprecated_globals={},
+                )
+
+        for use_http_post in [True, False]:
+            with self.assertRaises(
+                urllib.error.HTTPError,
+            ):
+                self.assert_graphql_query_result(
+                    Q,
+                    {'GlobalTest': [{'gdef': 'x', 'gdef2': 'x'}]},
+                    use_http_post=use_http_post,
+                    globals={},
+                    deprecated_globals={
+                        'default::test_global_def': 'x',
+                        'default::test_global_def2': 'x',
+                    },
+                )
+
     def test_graphql_func_01(self):
         Q = r'''query { FuncTest { fstr } }'''